Skip to content

Commit 27e758d

Browse files
authored
Merge pull request #560 from fractal-analytics-platform/test-improvements
Improved stability of end to end tests
2 parents f56f03b + f52adac commit 27e758d

File tree

11 files changed

+161
-62
lines changed

11 files changed

+161
-62
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
*Note: Numbers like (\#123) point to closed Pull Requests on the fractal-web repository.*
22

3+
# Unreleased
4+
5+
* Improved stability of end to end tests (\#560);
6+
37
# 1.6.0
48

59
* Displayed OAuth2 accounts in user profile page (\#557);

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/components/v2/jobs/JobsList.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,10 @@
427427
<td>
428428
{#if projects && row.project_id !== null && row.user_email === $page.data.userInfo.email}
429429
<a href={`/v2/projects/${row.project_id}`}>
430-
{projects.find((project) => project.id === row.project_id)?.name}
430+
{row.project_dump.name}
431431
</a>
432432
{:else}
433-
{projects.find((project) => project.id === row.project_id)?.name || '-'}
433+
{row.project_dump.name || '-'}
434434
{/if}
435435
</td>
436436
{/if}

tests/fake-job-server.js

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ const PORT = process.env.PORT || 8080;
44

55
const server = createServer();
66

7-
const tasksStatusMap = new Map();
7+
/** @type {Map<number, string>} */
8+
const jobsStatusMapV1 = new Map();
9+
/** @type {Map<number, string>} */
10+
const jobsStatusMapV2 = new Map();
811

912
server.on('request', (request, response) => {
1013
response.setHeader('Content-type', 'text/plain');
@@ -20,46 +23,65 @@ server.on('request', (request, response) => {
2023
if (!task) {
2124
return badRequest(response, 'missing task identifier');
2225
}
23-
const status = getTaskStatus(task);
26+
const { version, jobId } = extractJobInfo(task);
27+
const status = getJobStatus(version, jobId);
2428
if (request.method === 'GET' && status !== 'submitted') {
25-
tasksStatusMap.delete(task);
29+
console.log(`Removing job ${jobId}`);
30+
getJobStatusMap(version).delete(jobId);
2631
}
2732
response.end(status);
2833
} else {
2934
// PUT method is called by Playwright tests to update the desired task status
30-
if (url.pathname.startsWith('/done')) {
31-
setRunningTasksTo('done');
32-
response.end('done');
33-
} else if (url.pathname.startsWith('/failed')) {
34-
setRunningTasksTo('failed');
35-
response.end('failed');
35+
const match = url.pathname.match(/\/(v1|v2)\/(\d+)/);
36+
if (!match) {
37+
return badRequest(response, `missing job identifier in ${url.pathname}`);
3638
}
39+
const jobId = Number(match[2]);
40+
const status = url.searchParams.get('status');
41+
if (!status) {
42+
return badRequest(response, 'missing status');
43+
}
44+
const version = url.pathname.startsWith('/v2') ? 'v2' : 'v1';
45+
console.log(`Setting job ${jobId} status to ${status}`);
46+
getJobStatusMap(version).set(Number(jobId), status);
47+
response.end(status);
3748
}
3849
});
3950

40-
function getTaskStatus(task) {
41-
if (!tasksStatusMap.has(task)) {
42-
console.log(`Creating entry for task ${task}`);
43-
// Set all the other running tasks to failed
44-
// If there are no flacky tests there shouldn't be other running tasks
45-
setRunningTasksTo('failed');
46-
// Add new task as submitted
47-
tasksStatusMap.set(task, 'submitted');
51+
/**
52+
* @param {string} taskFolder
53+
*/
54+
function extractJobInfo(taskFolder) {
55+
const match = taskFolder.match(/job_(\d+)/);
56+
if (!match) {
57+
throw new Error(`Unable to extract job id from ${taskFolder}`);
4858
}
49-
return tasksStatusMap.get(task);
59+
const version = taskFolder.includes('proj_v2') ? 'v2' : 'v1';
60+
return { jobId: Number(match[1]), version };
5061
}
5162

5263
/**
53-
* We handle only one running task, to simplify the fake server.
54-
* In case of flacky Playwright tests there could be more than one submitted task.
55-
* In that case we set all the submitted task to the same desired final value.
56-
* @param {string} status
64+
* @param {string} version
65+
* @param {number} jobId
66+
* @returns {string|undefined}
5767
*/
58-
function setRunningTasksTo(status) {
59-
for (const [key, value] of tasksStatusMap.entries()) {
60-
if (value === 'submitted') {
61-
tasksStatusMap.set(key, status);
62-
}
68+
function getJobStatus(version, jobId) {
69+
const jobsStatusMap = getJobStatusMap(version);
70+
if (!jobsStatusMap.has(jobId)) {
71+
console.log(`Creating entry for job ${jobId}`);
72+
jobsStatusMap.set(jobId, 'submitted');
73+
}
74+
return jobsStatusMap.get(jobId);
75+
}
76+
77+
/**
78+
* @param {string} version
79+
*/
80+
function getJobStatusMap(version) {
81+
if (version === 'v1') {
82+
return jobsStatusMapV1;
83+
} else {
84+
return jobsStatusMapV2;
6385
}
6486
}
6587

tests/v1/admin_jobs.spec.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitPageLoading } from '../utils.js';
2+
import { waitModalClosed, waitPageLoading } from '../utils.js';
33
import { PageWithWorkflow } from './workflow_fixture.js';
44
import * as fs from 'fs';
55

66
test('Execute a job and show it on the job tables [v1]', async ({ page, request }) => {
77
/** @type {PageWithWorkflow} */
88
let workflow1;
99
await test.step('Create first job and wait its failure', async () => {
10-
workflow1 = await createJob(page, request, 'input', 'output');
11-
await workflow1.triggerTaskFailure();
10+
const job = await createJob(page, request, 'input', 'output');
11+
workflow1 = job.workflow;
12+
await workflow1.triggerTaskFailure(job.jobId);
1213
const jobBadge = page.locator('.badge.text-bg-danger');
1314
await jobBadge.waitFor();
1415
expect(await jobBadge.innerText()).toEqual('failed');
@@ -17,8 +18,12 @@ test('Execute a job and show it on the job tables [v1]', async ({ page, request
1718

1819
/** @type {PageWithWorkflow} */
1920
let workflow2;
21+
/** @type {number} */
22+
let jobId2;
2023
await test.step('Create second job', async () => {
21-
workflow2 = await createJob(page, request, 'input2', 'output2');
24+
const job = await createJob(page, request, 'input2', 'output2');
25+
workflow2 = job.workflow;
26+
jobId2 = job.jobId;
2227
});
2328

2429
await test.step('Open the admin jobs', async () => {
@@ -84,7 +89,7 @@ test('Execute a job and show it on the job tables [v1]', async ({ page, request
8489
});
8590

8691
await test.step('Wait job completion', async () => {
87-
await workflow2.triggerTaskSuccess();
92+
await workflow2.triggerTaskSuccess(jobId2);
8893
const jobBadge = page.locator('.badge.text-bg-success');
8994
await jobBadge.waitFor();
9095
expect(await jobBadge.innerText()).toEqual('done');
@@ -161,19 +166,22 @@ async function getWorkflowRow(page, workflowName) {
161166
* @param {import('@playwright/test').APIRequestContext} request
162167
* @param {string} inputDataset
163168
* @param {string} outputDataset
169+
* @returns {Promise<{ workflow: PageWithWorkflow, jobId: number }>}
164170
*/
165171
async function createJob(page, request, inputDataset, outputDataset) {
166172
const workflow = new PageWithWorkflow(page, request);
173+
/** @type {number|undefined} */
174+
let jobId;
167175
await test.step('Create workflow', async () => {
168176
await workflow.createProject();
169177
await workflow.createDataset(inputDataset, 'image');
170178
await workflow.createDataset(outputDataset, 'zarr');
171179
await workflow.createWorkflow();
172180
await page.waitForURL(/** @type {string} */ (workflow.url));
173181
await addTaskToWorkflow(workflow);
174-
await runWorkflow(page, inputDataset, outputDataset);
182+
jobId = await runWorkflow(page, inputDataset, outputDataset);
175183
});
176-
return workflow;
184+
return { workflow, jobId: /** @type {number} */ (jobId) };
177185
}
178186

179187
/**
@@ -189,8 +197,11 @@ async function addTaskToWorkflow(workflow) {
189197
* @param {import('@playwright/test').Page} page
190198
* @param {string} inputDataset
191199
* @param {string} outputDataset
200+
* @returns {Promise<number>} the id of the job
192201
*/
193202
async function runWorkflow(page, inputDataset, outputDataset) {
203+
/** @type {number|undefined} */
204+
let jobId;
194205
await test.step('Run workflow', async () => {
195206
const runWorkflowBtn = page.getByRole('button', { name: 'Run workflow' });
196207
await runWorkflowBtn.click();
@@ -204,5 +215,16 @@ async function runWorkflow(page, inputDataset, outputDataset) {
204215
const confirmBtn = page.locator('.modal.show').getByRole('button', { name: 'Confirm' });
205216
await confirmBtn.click();
206217
await page.waitForURL(new RegExp(`/v1/projects/(\\d+)/workflows/(\\d+)/jobs`));
218+
await page
219+
.getByRole('row', { name: 'submitted' })
220+
.getByRole('button', { name: 'Info' })
221+
.click();
222+
const modal = page.locator('.modal.show');
223+
await modal.waitFor();
224+
const value = await modal.getByRole('listitem').nth(1).textContent();
225+
jobId = Number(value?.trim());
226+
await modal.getByRole('button', { name: 'Close' }).click();
227+
await waitModalClosed(page);
207228
});
229+
return /** @type {number} */ (jobId);
208230
}

tests/v1/jobs.spec.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ test('Execute jobs', async ({ page, workflow }) => {
2525
});
2626

2727
await test.step('Check workflow jobs page', async () => {
28-
await page.waitForURL(`/v1/projects/${workflow.projectId}/workflows/${workflow.workflowId}/jobs`);
28+
await page.waitForURL(
29+
`/v1/projects/${workflow.projectId}/workflows/${workflow.workflowId}/jobs`
30+
);
2931
await page.locator('table tbody').waitFor();
3032
expect(await page.locator('table tbody tr').count()).toEqual(1);
3133
const cells = await page.locator('table tbody tr td').allInnerTexts();
@@ -82,7 +84,7 @@ test('Execute jobs', async ({ page, workflow }) => {
8284
const modalTitle = page.locator('.modal.show .modal-title');
8385
await modalTitle.waitFor();
8486
await expect(modalTitle).toHaveText('Workflow Job logs');
85-
await workflow.triggerTaskFailure();
87+
await workflow.triggerTaskFailure(jobId);
8688
await page.waitForFunction(() => {
8789
const modalBody = document.querySelector('.modal.show .modal-body');
8890
return modalBody instanceof HTMLElement && modalBody.innerText.includes('TASK ERROR');

tests/v1/workflow_fixture.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,26 @@ export class PageWithWorkflow extends PageWithProject {
7171
await this.addUserTask('Fake Task');
7272
}
7373

74-
async triggerTaskSuccess() {
75-
await this.setTaskStatus('done');
74+
/**
75+
* @param {number} jobId
76+
*/
77+
async triggerTaskSuccess(jobId) {
78+
await this.setTaskStatus(jobId, 'done');
7679
}
7780

78-
async triggerTaskFailure() {
79-
await this.setTaskStatus('failed');
81+
/**
82+
* @param {number} jobId
83+
*/
84+
async triggerTaskFailure(jobId) {
85+
await this.setTaskStatus(jobId, 'failed');
8086
}
8187

8288
/**
89+
* @param {number} jobId
8390
* @param {import('$lib/types.js').JobStatus} status
8491
*/
85-
async setTaskStatus(status) {
86-
const response = await this.request.put(`http://localhost:8080/${status}`);
92+
async setTaskStatus(jobId, status) {
93+
const response = await this.request.put(`http://localhost:8080/v1/${jobId}?status=${status}`);
8794
expect(response.ok()).toEqual(true);
8895
}
8996

tests/v2/admin_groups.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ test('Admin groups management', async ({ page }) => {
9595
}
9696
await modal.getByRole('button', { name: 'Add' }).click();
9797
await waitModalClosed(page);
98-
finalCount = selectableGroups.length + 2;
98+
finalCount = selectableGroups1 + 2;
9999
await expect(groupBadges).toHaveCount(finalCount);
100100
await expect(page.getByRole('button', { name: 'Add group' })).not.toBeVisible();
101101
});

0 commit comments

Comments
 (0)