diff --git a/frontend/e2e/helpers/editor.ts b/frontend/e2e/helpers/editor.ts new file mode 100644 index 00000000..f08a308a --- /dev/null +++ b/frontend/e2e/helpers/editor.ts @@ -0,0 +1,11 @@ +import type { Page } from '@playwright/test'; + +// The markdown editor is a ProseMirror contenteditable: fill() mutates the DOM directly and +// races the observer flush against the next action (e.g. a submit click), so the typed content +// can fail to land in the model. Real key events apply synchronously — always type through this. +export const typeIntoEditor = async (page: Page, name: string, text: string): Promise => { + const editor = page.getByRole('textbox', { name }); + + await editor.click(); + await editor.pressSequentially(text); +}; diff --git a/frontend/e2e/specs/auth.setup.ts b/frontend/e2e/specs/auth.setup.ts index dc56d84d..772b4310 100644 --- a/frontend/e2e/specs/auth.setup.ts +++ b/frontend/e2e/specs/auth.setup.ts @@ -16,7 +16,9 @@ setup('authenticate', async ({ page }) => { // The migration-seeded admin carries password_change_required — the forced // change screen is skippable and must not block the suite. const skip = page.getByRole('button', { name: 'Skip for now' }); - const loggedIn = page.getByRole('button', { name: /admin@|flows/i }); + // The user menu is labelled with the signed-in address, so derive the "logged in" signal + // from E2E_USER rather than a hardcoded admin@ (there is no button named "flows"). + const loggedIn = page.getByRole('button', { name: new RegExp(E2E_USER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') }); await expect(skip.or(loggedIn).first()).toBeVisible({ timeout: 15_000 }); diff --git a/frontend/e2e/specs/crud/knowledges.spec.ts b/frontend/e2e/specs/crud/knowledges.spec.ts index c2a83b13..daf0da8e 100644 --- a/frontend/e2e/specs/crud/knowledges.spec.ts +++ b/frontend/e2e/specs/crud/knowledges.spec.ts @@ -10,6 +10,7 @@ import type { import { ResultType } from '@/graphql/types'; import { expect, test } from '../../fixtures/test.ts'; +import { typeIntoEditor } from '../../helpers/editor.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; import { KNOWLEDGE_DOC, knowledgesCassette, makeKnowledge } from '../../mocks/cassettes/knowledges.ts'; @@ -28,7 +29,7 @@ test.describe('knowledges crud', { tag: '@crud' }, () => { test('blocks submit until the answer type is picked', async ({ page, pageErrorLog }) => { await openNewKnowledgeForm(page); await page.getByRole('textbox', { name: 'Question' }).fill('What is the E2E answer?'); - await page.getByRole('textbox', { name: 'Content' }).fill('E2E knowledge content'); + await typeIntoEditor(page, 'Content', 'E2E knowledge content'); await page.getByRole('button', { name: 'Create' }).click(); await expect(page.getByText('Answer type is required')).toBeVisible(); @@ -66,7 +67,7 @@ test.describe('knowledges crud', { tag: '@crud' }, () => { test('creates a document and lands on its detail page', async ({ page, pageErrorLog }) => { await openNewKnowledgeForm(page); await page.getByRole('textbox', { name: 'Question' }).fill('What is the E2E answer?'); - await page.getByRole('textbox', { name: 'Content' }).fill('E2E knowledge content'); + await typeIntoEditor(page, 'Content', 'E2E knowledge content'); await page.getByRole('combobox', { name: 'Answer type' }).click(); await page.getByRole('option', { name: 'other' }).click(); await page.getByRole('button', { name: 'Create' }).click(); diff --git a/frontend/e2e/specs/crud/resources.spec.ts b/frontend/e2e/specs/crud/resources.spec.ts index 5bb529d7..e16bbe33 100644 --- a/frontend/e2e/specs/crud/resources.spec.ts +++ b/frontend/e2e/specs/crud/resources.spec.ts @@ -23,7 +23,13 @@ test.describe('resources', { tag: '@coverage' }, () => { }); test.describe('mkdir', () => { - const added: ResultOf = { resourceAdded: CREATED_FOLDER }; + // Type a name distinct from the dialog's default so the request body proves the + // typed value reached it — a value equal to the default would match even if the + // input→payload binding were broken. + const TYPED_PATH = 'e2e-typed-folder'; + const added: ResultOf = { + resourceAdded: { ...CREATED_FOLDER, name: TYPED_PATH, path: TYPED_PATH }, + }; test.use({ cassette: resourcesCassette({ @@ -31,7 +37,7 @@ test.describe('resources', { tag: '@coverage' }, () => { 'POST /api/v1/resources/mkdir': [ { body: { data: {}, status: 'success' }, - bodySubset: { path: 'new-folder' }, + bodySubset: { path: TYPED_PATH }, setFlag: 'folder-created', }, ], @@ -50,10 +56,11 @@ test.describe('resources', { tag: '@coverage' }, () => { await expect(dialog.getByRole('heading', { name: 'Create directory' })).toBeVisible(); await expect(dialog.getByLabel('Path')).toHaveValue('new-folder'); + await dialog.getByLabel('Path').fill(TYPED_PATH); await dialog.getByRole('button', { name: 'Create' }).click(); await expect(page.getByText('Directory created')).toBeVisible(); - await expect(page.getByRole('treeitem', { name: /new-folder/ })).toBeVisible(); + await expect(page.getByRole('treeitem', { name: new RegExp(TYPED_PATH) })).toBeVisible(); expectCleanPage(pageErrorLog); }); }); diff --git a/frontend/e2e/specs/crud/templates.spec.ts b/frontend/e2e/specs/crud/templates.spec.ts index 44281d3b..17e81642 100644 --- a/frontend/e2e/specs/crud/templates.spec.ts +++ b/frontend/e2e/specs/crud/templates.spec.ts @@ -9,6 +9,7 @@ import type { import { ResultType } from '@/graphql/types'; import { expect, test } from '../../fixtures/test.ts'; +import { typeIntoEditor } from '../../helpers/editor.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; import { makeTemplate, TEMPLATE_SEED, templatesCassette } from '../../mocks/cassettes/templates.ts'; @@ -55,13 +56,7 @@ test.describe('templates crud', { tag: '@crud' }, () => { await expect(page).toHaveURL(/\/templates\/new$/); await page.getByLabel('Title').fill(NEW_TITLE); - - // fill() mutates the contenteditable DOM directly and races ProseMirror's - // observer flush against the submit click; real key events apply synchronously. - const editor = page.getByRole('textbox', { name: 'Template content' }); - - await editor.click(); - await editor.pressSequentially(NEW_TEXT); + await typeIntoEditor(page, 'Template content', NEW_TEXT); await page.getByRole('button', { name: 'Create' }).click(); await expect(page).toHaveURL(/\/templates$/);