(initial);
+ return (
+
+
+
+ {
+ stored = v;
+ setValue(v);
+ }}
+ defaultMode='text'
+ initialText={initialText}
+ expressions={[]}
+ />
+
+
+
+ );
+ };
+ return render();
+};
+
+/** Time passes: timers fire and what they started settles. */
+const pass = (ms: number) =>
+ act(async () => {
+ await vi.advanceTimersByTimeAsync(ms);
+ });
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ server.ready = false;
+ server.parses = [];
+ server.serializes = 0;
+ server.onReady = undefined;
+ server.onUnavailable = undefined;
+});
+afterEach(() => {
+ vi.useRealTimers();
+});
+
+describe('the Text view before its language server is ready', () => {
+ it('keeps what was typed, and reads it once the session is ready', async () => {
+ show({ is_expression: true, value: { args: [] } });
+ fireEvent.change(screen.getByTestId('fake-dpql'), { target: { value: '1 + 2' } });
+ // long past the debounce, and past the 8s the old loop gave the session
+ await pass(20000);
+ expect(stored?.value?.exp).toBeUndefined();
+ await act(async () => {
+ server.ready = true;
+ server.onReady?.();
+ });
+ await pass(1000);
+ expect(server.parses[server.parses.length - 1]).toBe('1 + 2');
+ expect(stored).toMatchObject({ is_expression: true, value: { exp: '+' } });
+ expect((screen.getByTestId('fake-dpql') as HTMLTextAreaElement).value).toBe('1 + 2');
+ });
+
+ it('asks nothing of a session that is not ready: no parse, no retry loop', async () => {
+ show(ONE_PLUS_TWO);
+ fireEvent.change(screen.getByTestId('fake-dpql'), { target: { value: '1 + 3' } });
+ await pass(20000);
+ expect(server.parses).toEqual([]);
+ expect(server.serializes).toBe(0);
+ });
+
+ it('writes the expression it opens with once the session is ready, however late', async () => {
+ show(ONE_PLUS_TWO);
+ await pass(20000);
+ await act(async () => {
+ server.ready = true;
+ server.onReady?.();
+ });
+ await pass(1000);
+ expect((screen.getByTestId('fake-dpql') as HTMLTextAreaElement).value).toBe('1 + 2');
+ });
+
+ it("keeps the host's own text: the session does not write over it once it is ready", async () => {
+ // the value written as text on the Value tab, handed to the Text view as it reads there
+ show(ONE_PLUS_TWO, 'concat("SUP-", $record:{pos})');
+ await act(async () => {
+ server.ready = true;
+ server.onReady?.();
+ });
+ await pass(1000);
+ expect((screen.getByTestId('fake-dpql') as HTMLTextAreaElement).value).toBe(
+ 'concat("SUP-", $record:{pos})'
+ );
+ expect(server.serializes).toBe(0);
+ });
+
+ it('says so when the server cannot be reached, and keeps what was typed', async () => {
+ const { container } = show({ is_expression: true, value: { args: [] } });
+ fireEvent.change(screen.getByTestId('fake-dpql'), { target: { value: '1 + 2' } });
+ await pass(1000);
+ await act(async () => {
+ server.onUnavailable?.();
+ });
+ await pass(100);
+ expect((screen.getByTestId('fake-dpql') as HTMLTextAreaElement).value).toBe('1 + 2');
+ expect(container.querySelector('.expression-text-unavailable')?.textContent).toContain(
+ 'not available'
+ );
+ });
+
+ it('shows the Visual view when the server cannot be reached and nothing was typed', async () => {
+ const { container } = show(ONE_PLUS_TWO);
+ await act(async () => {
+ server.onUnavailable?.();
+ });
+ await pass(100);
+ expect(screen.queryByTestId('fake-dpql')).toBeNull();
+ expect(container.querySelector('.expression-text-unavailable')).toBeTruthy();
+ });
+});
diff --git a/__tests__/expressionItemOwnProps.test.tsx b/__tests__/expressionItemOwnProps.test.tsx
new file mode 100644
index 00000000..e636acaf
--- /dev/null
+++ b/__tests__/expressionItemOwnProps.test.tsx
@@ -0,0 +1,55 @@
+/**
+ * An expression item's own props (`isChild`, `isAndOr`, `index`, `readOnly`) style it and decide its actions,
+ * and stop there (qorus#646).
+ *
+ * ExpressionItem read them, then spread every prop into its styled element, which the builder renders
+ * `as={ReqorePanel}`. A styled element rendered as a component forwards everything, so the panel received
+ * props it does not take, and `readOnly` was written onto the panel's `