forgejo/tests/e2e/issue-comment-dropzone.test.e2e.ts
Mathieu Fenniak 92684b6208 chore(e2e): test flakiness in issue-comment-dropzone.test.e2e.ts (#9660)
Flaky e2e test failure:
```
  1) [Mobile Chrome] › tests/e2e/issue-comment-dropzone.test.e2e.ts:74:1 › Re-add images to dropzone on edit
    Error: expect(locator).toHaveCount(expected) failed
    Locator:  locator('.dropzone').locator('.dz-preview')
    Expected: 1
    Received: 0
    Timeout:  3000ms
    Call log:
      - Expect "toHaveCount" with timeout 3000ms
      - waiting for locator('.dropzone').locator('.dz-preview')
        7 × locator resolved to 0 elements
          - unexpected value "0"
      87 |   await expect(dropzone.locator('.files').first()).toHaveCount(1);
      88 |   const preview = dropzone.locator('.dz-preview');
    > 89 |   await expect(preview).toHaveCount(1);
         |                         ^
      90 |   await expect(preview.locator('.dz-filename')).toHaveText('foo.png');
      91 |   await expect(preview.locator('.octicon-copy')).toBeVisible();
      92 |   await assertCopy(page, workerInfo, '![foo](');
        at /workspace/forgejo/forgejo/tests/e2e/issue-comment-dropzone.test.e2e.ts:89:25
```

Observed on chromium and Mobile Chrome.

I haven't been able to reproduce this test failure in local testing, but in examining the playwright test artifacts I noted that the browser is getting a `404 Not Found` error attempting to load a URL `(test server url)/uploading...` (where `...` is literally present in the URL).  My theory is that the test is firing the paste event in `pasteImage` and not waiting for the XMLHttpRequest to complete the upload, and then saving the issue comment with the placeholder URL `uploading...` in the Markdown, causing a later failure.  This patch adds two `waitForResponse` calls -- one when pasting the image to wait for the upload to complete, and one which is probably redundant which waits for the `/attachments` GET while editing the comment.

If this test continues to be flaky, it may at least have a different error revealing more about its cause.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9660
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2025-10-13 08:43:29 +02:00

107 lines
4 KiB
TypeScript

// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// web_src/js/features/common-global.js
// web_src/js/features/comp/Paste.js
// web_src/js/features/repo-issue.js
// web_src/js/features/repo-legacy.js
// @watch end
import {expect, type Locator, type Page, type TestInfo} from '@playwright/test';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
async function pasteImage(el: Locator) {
await el.evaluate(async (el) => {
const base64 = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAHklEQVQoU2MUk1P7z0AGYBzViDvURgMHT4oaQoEDAFuJEu2fuGfhAAAAAElFTkSuQmCC`;
// eslint-disable-next-line no-restricted-syntax
const response = await fetch(base64);
const blob = await response.blob();
el.focus();
let pasteEvent = new Event('paste', {bubbles: true, cancelable: true});
pasteEvent = Object.assign(pasteEvent, {
clipboardData: {
items: [
{
kind: 'file',
type: 'image/png',
getAsFile() {
return new File([blob], 'foo.png', {type: blob.type});
},
},
],
},
});
el.dispatchEvent(pasteEvent);
});
}
async function assertCopy(page: Page, workerInfo: TestInfo, startWith: string) {
const project = workerInfo.project.name;
if (project === 'webkit' || project === 'Mobile Safari') return;
const dropzone = page.locator('.dropzone');
const preview = dropzone.locator('.dz-preview');
const copyLink = preview.locator('.octicon-copy').locator('..');
await copyLink.click();
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardContent).toContain(startWith);
}
test('Paste image in new comment', async ({page}, workerInfo) => {
await page.goto('/user2/repo1/issues/new');
const waitForAttachmentUpload = page.waitForResponse((response) => {
return response.request().method() === 'POST' && response.url().endsWith('/attachments');
});
await pasteImage(page.locator('.markdown-text-editor'));
await waitForAttachmentUpload;
const dropzone = page.locator('.dropzone');
await expect(dropzone.locator('.files')).toHaveCount(1);
const preview = dropzone.locator('.dz-preview');
await expect(preview).toHaveCount(1);
await expect(preview.locator('.dz-filename')).toHaveText('foo.png');
await expect(preview.locator('.octicon-copy')).toBeVisible();
await assertCopy(page, workerInfo, '![foo](');
await screenshot(page, page.locator('.issue-content-left'));
});
test('Re-add images to dropzone on edit', async ({page}, workerInfo) => {
await page.goto('/user2/repo1/issues/new');
const issueTitle = dynamic_id();
await page.locator('#issue_title').fill(issueTitle);
const waitForAttachmentUpload = page.waitForResponse((response) => {
return response.request().method() === 'POST' && response.url().endsWith('/attachments');
});
await pasteImage(page.locator('.markdown-text-editor'));
await waitForAttachmentUpload;
await page.getByRole('button', {name: 'Create issue'}).click();
await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/);
await page.click('.comment-container .context-menu');
const waitForAttachmentsLoad = page.waitForResponse((response) => {
return response.request().method() === 'GET' && response.url().endsWith('/attachments');
});
await page.click('.comment-container .menu > .edit-content');
await waitForAttachmentsLoad;
const dropzone = page.locator('.dropzone');
await expect(dropzone.locator('.files').first()).toHaveCount(1);
const preview = dropzone.locator('.dz-preview');
await expect(preview).toHaveCount(1);
await expect(preview.locator('.dz-filename')).toHaveText('foo.png');
await expect(preview.locator('.octicon-copy')).toBeVisible();
await assertCopy(page, workerInfo, '![foo](');
await screenshot(page, page.locator('.issue-content-left'));
});