Skip to content

Commit f797342

Browse files
omacrangerLogan Graham
and
Logan Graham
authored
[JS] Add ability to assert against visual results, Add upload retry (#152)
Co-authored-by: Logan Graham <[email protected]>
1 parent b591196 commit f797342

File tree

20 files changed

+1300
-2497
lines changed

20 files changed

+1300
-2497
lines changed
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@saucelabs/visual-playwright": minor
3+
"@saucelabs/visual-storybook": minor
4+
"@saucelabs/cypress-visual-plugin": minor
5+
"@saucelabs/wdio-sauce-visual-service": minor
6+
"@saucelabs/visual": minor
7+
---
8+
9+
Unify result checking behavior into utility
10+
Add automatic retry mechanism to file uploads for timeouts

visual-js/visual-cypress/Dockerfile

+5-13
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,14 @@ FROM cypress/included:latest AS runner
22

33
WORKDIR app
44

5-
COPY package.json .
5+
RUN corepack enable
66

7-
COPY ./visual-cypress/src ./visual-cypress/src
8-
COPY ./visual-cypress/tsconfig.json ./visual-cypress/tsconfig.json
9-
COPY ./visual-cypress/package.json ./visual-cypress/package.json
7+
COPY . ./
108

11-
RUN npm install --workspace=visual-cypress
12-
RUN npm run build --workspace=visual-cypress
9+
RUN yarn install && npm run build --workspaces --if-present
1310

14-
COPY ./visual-cypress/integration-tests/cypress ./integration-tests/cypress
15-
COPY ./visual-cypress/integration-tests/cypress.config.js ./integration-tests/cypress.config.js
16-
COPY ./visual-cypress/integration-tests/package.json ./integration-tests/package.json
17-
COPY ./visual-cypress/integration-tests/tsconfig.json ./integration-tests/tsconfig.json
18-
19-
WORKDIR integration-tests
11+
WORKDIR ./visual-cypress/integration-tests
2012

2113
RUN npm install
2214

23-
ENTRYPOINT ["npm", "run", "login-test"]
15+
ENTRYPOINT ["npm", "run", "login-test"]

visual-js/visual-cypress/src/commands.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
VisualCheckOptions,
1717
VisualRegion,
1818
} from './types';
19+
import type { DiffStatus } from '@saucelabs/visual';
1920

2021
declare global {
2122
namespace Cypress {
@@ -33,7 +34,7 @@ declare global {
3334
options?: VisualCheckOptions,
3435
): Chainable<Subject>;
3536

36-
sauceVisualResults(): Chainable<Subject>;
37+
sauceVisualResults(): Chainable<Record<DiffStatus, number>>;
3738
}
3839
interface EndToEndConfigOptions {
3940
saucelabs: SauceConfig;

visual-js/visual-cypress/src/index.ts

+5-29
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
selectiveRegionOptionsToDiffingOptions,
2020
VisualApi,
2121
VisualApiRegion,
22+
getVisualResults,
2223
} from '@saucelabs/visual';
2324
import {
2425
HasSauceConfig,
@@ -265,35 +266,10 @@ Sauce Labs Visual: Unable to create new build.
265266
}
266267

267268
private async getResultSummary(): Promise<Record<DiffStatus, number>> {
268-
const diffsForTestResult = await this.api.diffsForTestResult(this.buildId!);
269-
if (!diffsForTestResult) {
270-
throw new Error('Something went wrong while fetching test results');
271-
}
272-
273-
const filterDiffsById = (diff: { id: string; status: DiffStatus }) =>
274-
this.uploadedDiffIds.includes(diff.id);
275-
const initialStatusSummary = {
276-
[DiffStatus.Queued]: 0,
277-
[DiffStatus.Unapproved]: 0,
278-
[DiffStatus.Approved]: 0,
279-
[DiffStatus.Equal]: 0,
280-
[DiffStatus.Errored]: 0,
281-
[DiffStatus.Rejected]: 0,
282-
} satisfies Record<DiffStatus, number>;
283-
const statusSummary = diffsForTestResult.nodes
284-
.filter(filterDiffsById)
285-
.reduce((statusSummary, diff) => {
286-
if (!statusSummary[diff.status]) {
287-
statusSummary[diff.status] = 0;
288-
}
289-
statusSummary[diff.status]++;
290-
return statusSummary;
291-
}, initialStatusSummary);
292-
if (statusSummary[DiffStatus.Queued]) {
293-
throw new DiffNotReadyError('Some diffs are not ready');
294-
}
295-
296-
return statusSummary;
269+
return await getVisualResults(this.api, {
270+
buildId: this.buildId,
271+
diffIds: this.uploadedDiffIds,
272+
});
297273
}
298274

299275
async getTestResults(): Promise<Record<DiffStatus, number>> {

visual-js/visual-playwright/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
"test": "jest --collect-coverage"
3434
},
3535
"dependencies": {
36-
"@playwright/test": "^1.42.1",
3736
"@saucelabs/visual": "^0.9.0",
3837
"exponential-backoff": "^3.1.1"
3938
},
39+
"peerDependencies": {
40+
"@playwright/test": "^1.42.1"
41+
},
4042
"tsup": {
4143
"entry": [
4244
"./src/index.ts"
@@ -54,6 +56,7 @@
5456
},
5557
"devDependencies": {
5658
"@jest/globals": "^28.0.0 || ^29.0.0",
59+
"@playwright/test": "^1.48.0",
5760
"@storybook/types": "^8.0.2",
5861
"@tsconfig/node18": "^2.0.0",
5962
"@types/node": "^18.13.0",

visual-js/visual-playwright/src/api.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
downloadDomScript,
55
getApi as getVisualApi,
66
getDomScript,
7+
getVisualResults,
78
RegionIn,
89
removeDomScriptFile,
910
VisualEnvOpts,
@@ -16,6 +17,7 @@ const clientVersion = 'PKG_VERSION';
1617

1718
export class VisualPlaywright {
1819
constructor(public client: string = `visual-playwright/${clientVersion}`) {}
20+
uploadedDiffIds: Record<string, string[]> = {};
1921

2022
public get api() {
2123
let api = globalThis.visualApi;
@@ -135,11 +137,12 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
135137
testName: string | undefined;
136138
suiteName: string | undefined;
137139
deviceName: string | undefined;
140+
testId: string;
138141
},
139142
name: string,
140143
options?: Partial<SauceVisualParams>,
141144
) {
142-
const { testName, suiteName, deviceName } = info;
145+
const { testName, suiteName, deviceName, testId } = info;
143146
const { buildId } = getOpts();
144147

145148
if (!buildId) {
@@ -156,7 +159,13 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
156159
ignoreRegions: userIgnoreRegions,
157160
diffingMethod,
158161
} = options ?? {};
159-
const { animations = 'disabled', caret } = screenshotOptions;
162+
const {
163+
animations = 'disabled',
164+
caret,
165+
fullPage = true,
166+
style,
167+
timeout,
168+
} = screenshotOptions;
160169
let ignoreRegions: RegionIn[] = [];
161170

162171
const promises: Promise<unknown>[] = [
@@ -270,7 +279,9 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
270279
const devicePixelRatio = await page.evaluate(() => window.devicePixelRatio);
271280

272281
const screenshotBuffer = await page.screenshot({
273-
fullPage: true,
282+
fullPage,
283+
style,
284+
timeout,
274285
animations,
275286
caret,
276287
clip,
@@ -318,12 +329,24 @@ ${e instanceof Error ? e.message : JSON.stringify(e)}
318329
diffingMethod,
319330
});
320331

321-
await this.api.createSnapshot({
332+
const { diffs } = await this.api.createSnapshot({
322333
...meta,
323334
testName,
324335
suiteName,
325336
uploadUuid: uploadId,
326337
});
338+
339+
if (!this.uploadedDiffIds[testId]) this.uploadedDiffIds[testId] = [];
340+
341+
this.uploadedDiffIds[testId].push(...diffs.nodes.map((diff) => diff.id));
342+
}
343+
344+
public async visualResults({ testId }: { testId: string }) {
345+
const { buildId } = getOpts();
346+
return await getVisualResults(this.api, {
347+
buildId,
348+
diffIds: this.uploadedDiffIds[testId] ?? [],
349+
});
327350
}
328351

329352
/**

visual-js/visual-playwright/src/fixtures.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
import { TestFixture, TestInfo } from '@playwright/test';
2-
import { Page } from 'playwright-core';
3-
import { SauceVisualParams } from './types';
4-
import { sauceVisualCheck } from './playwright';
1+
import type { TestFixture, TestInfo } from '@playwright/test';
2+
import type { Page } from 'playwright-core';
3+
import type { SauceVisualParams } from './types';
4+
import { sauceVisualCheck, sauceVisualResults } from './playwright';
5+
import type { DiffStatus } from '@saucelabs/visual';
56

67
export type SauceVisualFixtures = {
78
sauceVisual: {
9+
/**
10+
* Takes a snapshot of the current page and uploads it to Sauce Labs for visual diffing.
11+
* @param name
12+
* @param options
13+
*/
814
visualCheck: (name: string, options?: SauceVisualParams) => Promise<void>;
15+
/**
16+
* Returns the visual results for the active test. Can only be used inside a
17+
* `test` or `afterEach` block since it uses the current test context for finding matching
18+
* visual results.
19+
*/
20+
sauceVisualResults: () => Promise<Record<DiffStatus, number>>;
921
};
1022
};
1123

@@ -20,6 +32,7 @@ export const sauceVisualFixtures: (defaultOptions?: SauceVisualParams) => {
2032
...options,
2133
});
2234
},
35+
sauceVisualResults: async () => await sauceVisualResults(testInfo),
2336
});
2437
},
2538
});

visual-js/visual-playwright/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {
22
sauceVisualSetup,
33
sauceVisualTeardown,
44
sauceVisualCheck,
5+
sauceVisualResults,
56
} from './playwright';
67
export { sauceVisualFixtures, SauceVisualFixtures } from './fixtures';
78
export { SauceVisualParams } from './types';

visual-js/visual-playwright/src/playwright.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import VisualPlaywright from './api';
22
import type { Page } from 'playwright-core';
3-
import { TestInfo } from '@playwright/test';
4-
import { SauceVisualParams } from './types';
5-
import { VisualEnvOpts } from '@saucelabs/visual';
3+
import type { TestInfo } from '@playwright/test';
4+
import type { SauceVisualParams } from './types';
5+
import type { VisualEnvOpts } from '@saucelabs/visual';
66

77
export const sauceVisualSetup = async (opts?: Partial<VisualEnvOpts>) => {
88
VisualPlaywright.globalSetup(opts);
99
return VisualPlaywright.setup();
1010
};
1111
export const sauceVisualTeardown =
1212
VisualPlaywright.teardown.bind(VisualPlaywright);
13+
14+
/**
15+
* Takes a snapshot of the current page and uploads it to Sauce Labs for visual diffing.
16+
*/
1317
export const sauceVisualCheck = async (
1418
page: Page,
1519
testInfo: TestInfo,
@@ -22,7 +26,18 @@ export const sauceVisualCheck = async (
2226
deviceName: testInfo.project.name,
2327
testName: testInfo.title,
2428
suiteName: testInfo.titlePath.slice(0, -1).join('/'),
29+
testId: testInfo.testId,
2530
},
2631
name,
2732
options,
2833
);
34+
35+
/**
36+
* Returns the visual results for the active test. Can only be used inside a
37+
* `test` or `afterEach` block since it uses the current test context for finding matching
38+
* visual results.
39+
*/
40+
export const sauceVisualResults = async (testInfo: TestInfo) =>
41+
await VisualPlaywright.visualResults({
42+
testId: testInfo.testId,
43+
});

visual-js/visual-playwright/src/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { DiffingMethod, RegionIn, VisualEnvOpts } from '@saucelabs/visual';
22
import { PageScreenshotOptions } from 'playwright-core';
33

44
export interface SauceVisualParams {
5-
screenshotOptions?: Pick<PageScreenshotOptions, 'animations' | 'caret'>;
5+
screenshotOptions?: Pick<
6+
PageScreenshotOptions,
7+
'animations' | 'caret' | 'fullPage' | 'style' | 'timeout'
8+
>;
69
/**
710
* Whether we should capture a dom snapshot.
811
*/

visual-js/visual-storybook/src/api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const postVisit = async (page: Page, context: TestContext) => {
3333
testName: undefined,
3434
suiteName: undefined,
3535
deviceName: deviceName || undefined,
36+
testId: context.id,
3637
},
3738
`${context.title}/${context.name}`,
3839
{

visual-js/visual-wdio/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"build": "tsc --declaration -p tsconfig.build.json",
5757
"watch": "tsc -w --declaration -p tsconfig.build.json",
5858
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
59-
"test": "jest --collect-coverage"
59+
"test": "jest --collect-coverage --passWithNoTests"
6060
},
6161
"publishConfig": {
6262
"access": "public"

visual-js/visual-wdio/src/SauceVisualService.spec.ts

-56
This file was deleted.

0 commit comments

Comments
 (0)