diff --git a/e2e/live/form-view-subforms.spec.ts b/e2e/live/form-view-subforms.spec.ts index 1b3c1bbc7..f1ae6a06b 100644 --- a/e2e/live/form-view-subforms.spec.ts +++ b/e2e/live/form-view-subforms.spec.ts @@ -4,9 +4,9 @@ import { selectOption, fillLookup, addLineItem } from './helpers'; /** * Tier 0 live e2e: an object's standard New/Edit modal renders inline child * collections derived from the DATA MODEL (no view config, no bespoke page). - * `showcase_task.project` declares `inlineEdit: true`, so every standard - * Project form auto-renders a Tasks subtable; "New Project" opens a - * master-detail modal that submits parent + children in one atomic + * `showcase_invoice_line.invoice` declares `inlineEdit: 'grid'`, so every + * standard Invoice form auto-renders a "Line Items" grid; "New Invoice" opens + * a master-detail modal that submits the header + its lines in one atomic * /api/v1/batch. (An explicit `form.subforms` would override the derived one.) */ test('New modal renders relationship-derived subforms and submits an atomic batch', async ({ page }) => { @@ -17,22 +17,22 @@ test('New modal renders relationship-derived subforms and submits an at } }); - await page.goto('/apps/showcase_app/showcase_project'); + await page.goto('/apps/showcase_app/showcase_invoice'); await page.getByRole('button', { name: /^New$/i }).first().click(); // The standard create modal is now a master-detail form (no custom page). const dialog = page.getByRole('dialog'); await expect(dialog.getByTestId('md-form-submit')).toBeVisible(); - await expect(dialog.getByText('Tasks', { exact: false })).toBeVisible(); + await expect(dialog.getByText('Line Items', { exact: false })).toBeVisible(); - const name = `Tier0 ${Date.now()}`; + const name = `INV-${Date.now()}`; await dialog.locator('input[name="name"]').fill(name); await fillLookup(page, 'account', 'North'); - await selectOption(page, 'status', 'planned'); + await selectOption(page, 'status', 'draft'); await expect(dialog.getByText('Northwind', { exact: false }).first()).toBeVisible(); const row = await addLineItem(page); - await row.getByRole('textbox').first().fill('Tier0 Task'); + await row.getByRole('textbox').first().fill('Widget A'); await Promise.all([ page.waitForRequest((r) => r.url().includes('/api/v1/batch'), { timeout: 15_000 }).catch(() => null), @@ -42,9 +42,9 @@ test('New modal renders relationship-derived subforms and submits an at expect(batches.length).toBeGreaterThan(0); const ops = batches[0].operations; - expect(ops[0]).toMatchObject({ object: 'showcase_project', action: 'create' }); + expect(ops[0]).toMatchObject({ object: 'showcase_invoice', action: 'create' }); expect(ops[0].data.name).toBe(name); - expect(ops[0].data.status).toBe('planned'); - const child = ops.find((o: any) => o.object === 'showcase_task'); - expect(child?.data?.project).toEqual({ $ref: 0 }); + expect(ops[0].data.status).toBe('draft'); + const child = ops.find((o: any) => o.object === 'showcase_invoice_line'); + expect(child?.data?.invoice).toEqual({ $ref: 0 }); }); diff --git a/e2e/live/master-detail.spec.ts b/e2e/live/master-detail.spec.ts index f37ab659b..b030d125f 100644 --- a/e2e/live/master-detail.spec.ts +++ b/e2e/live/master-detail.spec.ts @@ -63,8 +63,17 @@ test('Create with a task line includes the child op referencing the parent', asy await selectOption(page, 'status', 'active'); await expect(page.getByText('Northwind', { exact: false })).toBeVisible(); - const row = await addLineItem(page); - await row.getByRole('textbox').first().fill('E2E Task A'); + // `showcase_task` is a rich child, so its relationship smart-default resolves + // to the per-row `form` factor: "Add task" opens the child's full form + // inline (not editable grid cells). Fill the required fields and Apply to + // stage the row, then save the whole thing as one atomic batch. + await page.getByTestId('line-items-add').click(); + const editor = page.getByTestId('md-row-form'); + await expect(editor).toBeVisible(); + await editor.locator('input[name="title"]').fill('E2E Task A'); + await selectOption(editor, 'status', 'backlog'); + await editor.getByRole('button', { name: 'Apply', exact: true }).click(); + await expect(editor).toBeHidden(); await Promise.all([ page.waitForRequest((r) => r.url().includes('/api/v1/batch'), { timeout: 15_000 }).catch(() => null), @@ -78,5 +87,6 @@ test('Create with a task line includes the child op referencing the parent', asy // a child task op referencing the parent via $ref:0 const child = ops.find((o: any) => o.object === 'showcase_task'); expect(child).toBeTruthy(); + expect(child?.data?.title).toBe('E2E Task A'); expect(child?.data?.project).toEqual({ $ref: 0 }); }); diff --git a/e2e/live/row-expand.spec.ts b/e2e/live/row-expand.spec.ts index 848636a4e..7556b58ba 100644 --- a/e2e/live/row-expand.spec.ts +++ b/e2e/live/row-expand.spec.ts @@ -1,13 +1,12 @@ import { test, expect } from '@playwright/test'; -import { selectOption } from './helpers'; /** * Live e2e for the master-detail inline grid's per-row "expand to full form" * (the mainstream hybrid: a quick grid + a rich per-row form). Adding a line - * then clicking its expand button reveals the child's COMPLETE form inline - * (rich types the grid omits live here; the parent FK is excluded). Applying - * writes the values back into the grid row — no separate backend write; the - * atomic batch still persists everything on the parent Save. + * then clicking its expand button reveals the child's COMPLETE form inline, + * with the parent relationship FK excluded. Applying writes the values back + * into the grid row — no separate backend write; the atomic batch still + * persists everything on the parent Save. * * The editor is rendered inline (not a portaled drawer) precisely so it stays * interactive + accessible when this form is itself inside the create-record @@ -15,30 +14,30 @@ import { selectOption } from './helpers'; * pointer-events / aria-hidden lock and be unclickable). */ test('a grid row expands into a full inline form and writes values back', async ({ page }) => { - await page.goto('/apps/showcase_app/showcase_project'); + await page.goto('/apps/showcase_app/showcase_invoice'); await page.getByRole('button', { name: /^New$/i }).first().click(); const dialog = page.getByRole('dialog').first(); await expect(dialog.getByTestId('md-form-submit')).toBeVisible(); - // Add a task line, then expand it into the full form. + // Add a line item, then expand it into the full form. await dialog.getByTestId('line-items-add').click(); await dialog.getByTestId('line-items-expand-0').click(); const editor = page.getByTestId('md-row-form'); await expect(editor).toBeVisible(); - // Richer than the grid: includes fields the grid omits (Notes) and excludes - // the parent relationship FK (Project). - await expect(editor.getByText('Notes', { exact: false }).first()).toBeVisible(); - await expect(editor.getByText(/^Project$/)).toHaveCount(0); + // The full form includes the line's business fields (Product) and excludes + // the parent relationship FK (Invoice). + await expect(editor.getByText('Product', { exact: false }).first()).toBeVisible(); + await expect(editor.getByText(/^Invoice$/)).toHaveCount(0); - // Fill the required fields (Title + Status) in the full form, then Apply. - await editor.locator('input[name="title"]').fill('Deep task A'); - await selectOption(editor, 'status', 'todo'); + // Fill the required fields (Product + Qty) in the full form, then Apply. + await editor.locator('input[name="product"]').fill('Widget Deluxe'); + await editor.locator('input[name="quantity"]').fill('3'); await editor.getByRole('button', { name: 'Apply', exact: true }).click(); // Editor closes and the value is written back into the grid row. await expect(editor).toBeHidden(); - await expect(dialog.getByTestId('line-items').getByRole('textbox').first()).toHaveValue('Deep task A'); + await expect(dialog.getByTestId('line-items').getByRole('textbox').first()).toHaveValue('Widget Deluxe'); });