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
29 changes: 28 additions & 1 deletion e2e/helpers/worklog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,31 @@ export async function pickFirstOption(page: Page, row: Locator, colKey: string,
await expect(page.locator('.combobox-content')).toBeHidden({ timeout: 4000 });
}

/**
* The one seeded customer both e2e users can book (`Freizeit`, id 2 — global,
* with projects; see e2e/AGENTS.md and sql/testdata.sql). The worklog CUSTOMER
* pick must target it BY NAME, never "first option": the option list is
* name-sorted and shared run-wide, so a throwaway admin-spec customer
* (`E2E*`, global, without projects) that leaks past its best-effort delete
* sorts first — a blind first-option pick then selects it and the dependent
* project combobox renders empty ("Keine Treffer"), deterministically failing
* every retry (scheduled runs 29629898230 and 31767912809, shard 1).
*/
export const SEEDED_BOOKABLE_CUSTOMER = 'Freizeit';

/** Open a relation cell's combobox, filter it by `text`, and pick the first match. */
export async function pickOptionByText(page: Page, row: Locator, colKey: string, text: string): Promise<void> {
await row.locator(`td[data-col-key="${colKey}"]`).focus();
await page.keyboard.press('Enter');
const input = page.locator('.combobox-input').first();
await expect(input).toBeVisible();
await input.fill(text);
const option = page.locator('.combobox-content .combobox-item').filter({ hasText: text }).first();
await expect(option).toBeVisible({ timeout: 8000 });
await option.click();
await expect(page.locator('.combobox-content')).toBeHidden({ timeout: 4000 });
}

export function rowByStamp(page: Page, stamp: string): Locator {
return page.locator('tr.tracking-row').filter({ hasText: stamp }).first();
}
Expand Down Expand Up @@ -123,7 +148,9 @@ export async function createWorklogEntry(page: Page): Promise<string> {
// late-landing refetch (the historic boundingBox-null flake).
const saved = page.waitForResponse(isSaveResponse);
const refetched = page.waitForResponse(isEntriesRefetch);
await pickFirstOption(page, row, 'customer');
// Customer by NAME (leaked E2E* customers sort first — see SEEDED_BOOKABLE_CUSTOMER);
// project/activity are scoped to that customer, so first-option is deterministic.
await pickOptionByText(page, row, 'customer', SEEDED_BOOKABLE_CUSTOMER);
await pickFirstOption(page, row, 'project');
await pickFirstOption(page, row, 'activity');
await saved;
Expand Down
38 changes: 31 additions & 7 deletions e2e/worklog-grid-editing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { loginIsolated } from './helpers/auth';
import { installFrozenClock } from './helpers/clock';
import { goToWorklogPage } from './helpers/navigation';
import { cleanupWorklogEntries, createWorklogEntry, isSaveResponse, rowByStamp } from './helpers/worklog';
import { SEEDED_BOOKABLE_CUSTOMER, cleanupWorklogEntries, createWorklogEntry, isSaveResponse, rowByStamp } from './helpers/worklog';

/**
* Spreadsheet-style keyboard + clipboard editing on the SolidJS worklog grid:
Expand Down Expand Up @@ -136,8 +136,17 @@ test.describe('Worklog grid — keyboard & clipboard editing', () => {
await page.keyboard.press('Tab');
await expect(row.locator('td[data-col-key="customer"][data-inline-editing]')).toBeVisible();

const arrowEnter = async (): Promise<void> => {
await expect(page.locator('.combobox-input').first()).toBeVisible();
const arrowEnter = async (filter?: string): Promise<void> => {
const input = page.locator('.combobox-input').first();
await expect(input).toBeVisible();
// The customer step filters to the seeded bookable customer first — the raw
// list is name-sorted and a leaked E2E* throwaway customer (no projects)
// would sort first, so a blind ArrowDown would pick it and leave the
// subsequent project list empty (see SEEDED_BOOKABLE_CUSTOMER).
if (filter !== undefined) {
await input.fill(filter);
await expect(page.locator('.combobox-content .combobox-item').first()).toContainText(filter, { timeout: 8000 });
}
// Wait for the option list to populate before navigating it — under shard load
// ArrowDown can fire before the (async) options arrive, highlighting nothing, so
// Enter then picks nothing and the guide never advances to the next field.
Expand All @@ -148,7 +157,7 @@ test.describe('Worklog grid — keyboard & clipboard editing', () => {
// customer/project/activity are always required-and-empty for a new row, so the
// guide jumps through them. (date/start/end are pre-filled by suggest-time +
// the end-prefill minimum, so they're skipped — the guide only targets empties.)
await arrowEnter();
await arrowEnter(SEEDED_BOOKABLE_CUSTOMER);
await expect(row.locator('td[data-col-key="project"][data-inline-editing]')).toBeVisible();
await arrowEnter();
await expect(row.locator('td[data-col-key="activity"][data-inline-editing]')).toBeVisible();
Expand Down Expand Up @@ -176,14 +185,21 @@ test.describe('Worklog grid — keyboard & clipboard editing', () => {
await page.keyboard.press('Tab');
await expect(page.locator('td[data-col-key="customer"][data-inline-editing]')).toBeVisible();

const arrowEnter = async (): Promise<void> => {
await expect(page.locator('.combobox-input').first()).toBeVisible();
const arrowEnter = async (filter?: string): Promise<void> => {
const input = page.locator('.combobox-input').first();
await expect(input).toBeVisible();
// Filter the customer step to the seeded bookable customer (see the
// guided-flow test above and SEEDED_BOOKABLE_CUSTOMER).
if (filter !== undefined) {
await input.fill(filter);
await expect(page.locator('.combobox-content .combobox-item').first()).toContainText(filter, { timeout: 8000 });
}
// Wait for the options before arrowing (see the guided-flow test above).
await expect(page.locator('.combobox-content .combobox-item').first()).toBeVisible({ timeout: 8000 });
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
};
await arrowEnter(); // customer — guided on to project
await arrowEnter(SEEDED_BOOKABLE_CUSTOMER); // customer — guided on to project
await expect(page.locator('td[data-col-key="project"][data-inline-editing]')).toBeVisible();
await arrowEnter(); // project — guided on to activity
await expect(page.locator('td[data-col-key="activity"][data-inline-editing]')).toBeVisible();
Expand Down Expand Up @@ -223,6 +239,14 @@ test.describe('Worklog grid — keyboard & clipboard editing', () => {

await row.locator('td[data-col-key="customer"]').focus();
await page.keyboard.press('Enter'); // open the (portalled) select editor
// Filter to the seeded bookable customer before arrowing — a blind ArrowDown
// would pick whatever sorts first, and a leaked E2E* throwaway customer (no
// projects) would invalidate the row and re-render it, losing the very focus
// this test asserts on (see SEEDED_BOOKABLE_CUSTOMER).
const input = page.locator('.combobox-input').first();
await expect(input).toBeVisible();
await input.fill(SEEDED_BOOKABLE_CUSTOMER);
await expect(page.locator('.combobox-content .combobox-item').first()).toContainText(SEEDED_BOOKABLE_CUSTOMER, { timeout: 8000 });
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter'); // commit — focus must return to a grid cell, not <body>
await expect(page.locator('.tracking-table td:focus')).toHaveCount(1);
Expand Down