Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions e2e/live/form-view-subforms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <object> modal renders relationship-derived subforms and submits an atomic batch', async ({ page }) => {
Expand All @@ -17,22 +17,22 @@ test('New <object> 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),
Expand All @@ -42,9 +42,9 @@ test('New <object> 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 });
});
14 changes: 12 additions & 2 deletions e2e/live/master-detail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 });
});
29 changes: 14 additions & 15 deletions e2e/live/row-expand.spec.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
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
* modal (a nested portaled overlay would inherit the host modal's
* 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');
});
Loading