extend test cases for enhanced safety around event handlers

This commit is contained in:
Mathieu Fenniak 2025-03-29 18:35:48 -06:00 committed by Gusted
parent 84b2c48d0d
commit 7546fb083f
3 changed files with 100 additions and 6 deletions

View file

@ -5,11 +5,12 @@
{{template "base/alert" .}}
{{/* Refresh the list every interval (30s) unless the document isn't visible or a dropdown is open; refresh
if visibility changes as well. */}}
if visibility changes as well. simulate-polling-interval is a custom event used for e2e tests to mimic
the polling interval and should be defined identically to the `every` clause for accurate testing. */}}
<div
hx-get="?workflow={{$.CurWorkflow}}&actor={{$.CurActor}}&status={{$.CurStatus}}&page={{$.Page.Paginater.Current}}&list_inner=true"
hx-swap="morph:innerHTML"
hx-trigger="every 30s [document.visibilityState === 'visible' && noActiveDropdowns()], visibilitychange[document.visibilityState === 'visible'] from:document"
hx-trigger="every 30s [pollingOk()], visibilitychange[document.visibilityState === 'visible'] from:document, simulate-polling-interval[pollingOk()] from:document"
hx-indicator="#reloading-indicator">
{{template "repo/actions/list_inner" .}}
</div>
@ -18,6 +19,10 @@
<script type="text/javascript">
function pollingOk() {
return document.visibilityState === 'visible' && noActiveDropdowns();
}
// Intent: If the "Actor" or "Status" dropdowns are currently open and being navigated, or the workflow dispatch
// dropdown form is open, the htmx refresh would replace them with closed dropdowns. Instead this prevents the list
// refresh from occurring while those dropdowns are open.

View file

@ -23,7 +23,7 @@
<div id="reloading-indicator" class="htmx-indicator"></div>
<!-- Actor -->
<div class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
<div id="actor_dropdown" class="ui{{if not .Actors}} disabled{{end}} dropdown jump item">
<span class="text">{{ctx.Locale.Tr "actions.runs.actor"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
@ -42,7 +42,7 @@
</div>
</div>
<!-- Status -->
<div class="ui dropdown jump item">
<div id="status_dropdown" class="ui dropdown jump item">
<span class="text">{{ctx.Locale.Tr "actions.runs.status"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">

View file

@ -81,6 +81,18 @@ test('workflow dispatch box not available for unauthenticated users', async ({pa
await save_visual(page);
});
async function simulatePollingInterval(page) {
// In order to simulate the background page sitting around for > 30s, a custom event `simulate-polling-interval` is
// fired into the document to mimic the polling interval expiring -- although this isn't a perfectly great E2E test
// with this kind of mimicry, it's better than having multiple >30s execution-time tests.
// No other way to trigger a client-side event other than using browser-side eval:
// eslint-disable-next-line playwright/no-eval
await page.$eval('html', () => {
document.dispatchEvent(new Event('simulate-polling-interval'));
});
}
test.describe('workflow list dynamic refresh', () => {
test.use({user: 'user2'});
@ -99,10 +111,87 @@ test.describe('workflow list dynamic refresh', () => {
// No other way to trigger a visibilitychange event other than using browser-side eval:
// eslint-disable-next-line playwright/no-eval
await backgroundPage.$eval('html', () => {
const ev = new Event('visibilitychange');
document.dispatchEvent(ev);
document.dispatchEvent(new Event('visibilitychange'));
});
await expect(backgroundPage.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
await save_visual(backgroundPage);
});
test('refreshes on interval', async ({page}, testInfo) => {
// Test operates by creating two pages; one which is sitting idle on the workflows list (backgroundPage), and one
// which triggers a workflow dispatch. After the polling, the page should refresh and show the newly dispatched
// workflow from the other page.
const backgroundPage = await (await page.context()).newPage();
await backgroundPage.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
// Mirror the `Workflow Authenticated user2 > dispatch success` test:
await dispatchSuccess(page, testInfo);
await simulatePollingInterval(page);
await expect(backgroundPage.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
await save_visual(backgroundPage);
});
test('post-refresh the dropdowns continue to operate', async ({page}, testInfo) => {
// Verify that after the page is dynamically refreshed, the 'Actor', 'Status', and 'Run workflow' dropdowns work
// correctly -- that the htmx morph hasn't messed up any JS event handlers.
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
await simulatePollingInterval(page);
// Mirror the `Workflow Authenticated user2 > dispatch success` test -- this creates data for the 'Actor' dropdown
await dispatchSuccess(page, testInfo);
// Workflow run dialog
await expect(page.locator('input[name="inputs[string2]"]')).toBeHidden();
await page.locator('#workflow_dispatch_dropdown>button').click();
await expect(page.locator('input[name="inputs[string2]"]')).toBeVisible();
await page.locator('#workflow_dispatch_dropdown>button').click();
// Status dropdown
await expect(page.getByText('Waiting')).toBeHidden();
await expect(page.getByText('Failure')).toBeHidden();
await page.locator('#status_dropdown').click();
await expect(page.getByText('Waiting')).toBeVisible();
await expect(page.getByText('Failure')).toBeVisible();
// Actor dropdown
await expect(page.getByText('All actors')).toBeHidden();
await page.locator('#actor_dropdown').click();
await expect(page.getByText('All Actors')).toBeVisible();
});
test('refresh does not break interacting with open drop-downs', async ({page}) => {
// Verify that if the polling refresh occurs while interacting with any multi-step dropdown on the page, the
// multi-step interaction continues to be visible and functional. This is implemented by preventing the refresh,
// but that isn't the subject of the test here -- as long as the dropdown isn't broken by the refresh, that's fine.
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
// Mirror the `Workflow Authenticated user2 > dispatch success` test -- this creates data for the 'Actor' dropdown
// await dispatchSuccess(page, testInfo);
// Workflow run dialog
await expect(page.locator('input[name="inputs[string2]"]')).toBeHidden();
await page.locator('#workflow_dispatch_dropdown>button').click();
await expect(page.locator('input[name="inputs[string2]"]')).toBeVisible();
await simulatePollingInterval(page);
await expect(page.locator('input[name="inputs[string2]"]')).toBeVisible();
// Status dropdown
await expect(page.getByText('Waiting')).toBeHidden();
await expect(page.getByText('Failure')).toBeHidden();
await page.locator('#status_dropdown').click();
await expect(page.getByText('Waiting')).toBeVisible();
await expect(page.getByText('Failure')).toBeVisible();
await simulatePollingInterval(page);
await expect(page.getByText('Waiting')).toBeVisible();
await expect(page.getByText('Failure')).toBeVisible();
// Actor dropdown
await expect(page.getByText('All actors')).toBeHidden();
await page.locator('#actor_dropdown').click();
await expect(page.getByText('All Actors')).toBeVisible();
await simulatePollingInterval(page);
await expect(page.getByText('All Actors')).toBeVisible();
});
});