test(e2e): settle editor typing, resources input and the stand login detector

- Route both the knowledge and template editor inputs through a shared
  typeIntoEditor helper (pressSequentially), instead of fill() on one and a
  documented ProseMirror-race workaround on the other.
- Make the mkdir spec type a name distinct from the dialog default and pin it, so
  a broken input→payload binding no longer matches the default.
- Derive the stand login-readiness locator from E2E_USER instead of a hardcoded
  admin@ (and a "flows" button that does not exist).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-22 05:54:14 +07:00
co-authored by Claude Opus 4.8
parent 5803d5adc6
commit 06f53c72a0
5 changed files with 29 additions and 13 deletions
+11
View File
@@ -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<void> => {
const editor = page.getByRole('textbox', { name });
await editor.click();
await editor.pressSequentially(text);
};
+3 -1
View File
@@ -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 });
+3 -2
View File
@@ -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();
+10 -3
View File
@@ -23,7 +23,13 @@ test.describe('resources', { tag: '@coverage' }, () => {
});
test.describe('mkdir', () => {
const added: ResultOf<typeof ResourceAddedDocument> = { 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<typeof ResourceAddedDocument> = {
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);
});
});
+2 -7
View File
@@ -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$/);