Skip to content

Commit 8572e1b

Browse files
add scrollElement fps option for native apps (#140)
1 parent 93c7f19 commit 8572e1b

File tree

11 files changed

+138
-75
lines changed

11 files changed

+138
-75
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@saucelabs/nightwatch-sauce-visual-service": minor
3+
"@saucelabs/wdio-sauce-visual-service": minor
4+
"@saucelabs/visual": minor
5+
---
6+
7+
scrollElement native fps

visual-js/visual-nightwatch/Dockerfile

+6-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ WORKDIR app
44

55
COPY tsconfig.prod.json tsconfig.json package.json ./
66

7+
COPY ./visual/src ./visual/src
8+
COPY ./visual/package.json ./visual/tsconfig.json ./visual/
79
COPY ./visual-nightwatch/src ./visual-nightwatch/src
8-
COPY ./visual-nightwatch/package.json ./visual-nightwatch/tsconfig.json ./visual-nightwatch
10+
COPY ./visual-nightwatch/package.json ./visual-nightwatch/tsconfig.json ./visual-nightwatch/
911

1012
RUN npm install --save-dev @tsconfig/node18
13+
RUN npm install --workspace=visual
14+
RUN npm run build --workspace=visual
1115
RUN npm install --workspace=visual-nightwatch
1216
RUN npm run build --workspace=visual-nightwatch
1317

@@ -22,4 +26,4 @@ WORKDIR integration-tests
2226

2327
RUN npm install
2428

25-
ENTRYPOINT ["npm", "run", "external"]
29+
ENTRYPOINT ["npm", "run", "external"]

visual-js/visual-nightwatch/src/nightwatch/commands/sauceVisualCheck.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
} from '@saucelabs/visual';
1111
import { getMetaInfo, getVisualApi } from '../../utils/api';
1212
import { VISUAL_BUILD_ID_KEY } from '../../utils/constants';
13-
import { NightwatchAPI, NightwatchCustomCommandsModel } from 'nightwatch';
13+
import {
14+
NightwatchAPI,
15+
NightwatchCustomCommandsModel,
16+
ScopedElement,
17+
} from 'nightwatch';
1418
import { CheckOptions, NightwatchIgnorable, RunnerSettings } from '../../types';
1519
import type { Runnable } from 'mocha';
1620

@@ -148,7 +152,11 @@ class SauceVisualCheck implements NightwatchCustomCommandsModel {
148152
sessionMetadata: metaInfo,
149153
suiteName,
150154
testName,
151-
fullPageConfig: getFullPageConfig(fullPage, options.fullPage),
155+
fullPageConfig: await getFullPageConfig<ScopedElement>(
156+
fullPage,
157+
options.fullPage,
158+
async (el) => await el.getId(),
159+
),
152160
clipElement:
153161
(await options.clipElement?.getId()) ?? clipElementFromClipSelector,
154162
captureDom: options.captureDom ?? globalCaptureDom,

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface CheckOptions {
3131
ignore?: NightwatchIgnorable[];
3232
regions?: RegionType<ElementType>[];
3333
diffingMethod?: DiffingMethod;
34-
fullPage?: FullPageScreenshotOptions;
34+
fullPage?: FullPageScreenshotOptions<ScopedElement>;
3535
/**
3636
* Whether we should take a snapshot of the DOM to compare with as a part of the diffing process.
3737
*/

visual-js/visual-wdio/Dockerfile

+5-18
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,14 @@ FROM node:18 AS runner
22

33
WORKDIR app
44

5-
COPY tsconfig.prod.json .
6-
COPY tsconfig.json .
7-
COPY package.json .
5+
RUN corepack enable
86

9-
COPY ./visual-wdio/src ./visual-wdio/src
10-
COPY ./visual-wdio/package.json ./visual-wdio/package.json
11-
COPY ./visual-wdio/tsconfig.json ./visual-wdio/tsconfig.json
12-
COPY ./visual-wdio/tsconfig.build.json ./visual-wdio/tsconfig.build.json
7+
COPY . ./
138

14-
RUN npm install --workspace=visual-wdio
15-
RUN npm run build --workspace=visual-wdio
9+
RUN yarn install && npm run build --workspaces --if-present
1610

17-
COPY ./visual-wdio/integration-tests/configs ./integration-tests/configs
18-
COPY ./visual-wdio/integration-tests/helpers ./integration-tests/helpers
19-
COPY ./visual-wdio/integration-tests/pages ./integration-tests/pages
20-
COPY ./visual-wdio/integration-tests/specs ./integration-tests/specs
21-
22-
COPY ./visual-wdio/integration-tests/package.json ./integration-tests/package.json
23-
24-
WORKDIR integration-tests
11+
WORKDIR ./visual-wdio/integration-tests
2512

2613
RUN npm install
2714

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

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

+14-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import {
2828

2929
import logger from '@wdio/logger';
3030
import chalk from 'chalk';
31-
import { Ignorable, isWdioElement, WdioElement } from './guarded-types.js';
31+
import {
32+
FullPageScreenshotWdioOptions,
33+
Ignorable,
34+
isWdioElement,
35+
WdioElement,
36+
} from './guarded-types.js';
3237
import { backOff } from 'exponential-backoff';
3338
import type { Test } from '@wdio/types/build/Frameworks';
3439

@@ -95,7 +100,7 @@ export type SauceVisualServiceOptions = {
95100
clipSelector?: string;
96101
clipElement?: WdioElement;
97102
region?: SauceRegion;
98-
fullPage?: FullPageScreenshotOptions;
103+
fullPage?: FullPageScreenshotWdioOptions;
99104
baselineOverride?: BaselineOverrideIn;
100105
};
101106

@@ -131,7 +136,7 @@ export type CheckOptions = {
131136
captureDom?: boolean;
132137
diffingMethod?: DiffingMethod;
133138
disable?: (keyof DiffingOptionsIn)[];
134-
fullPage?: FullPageScreenshotOptions;
139+
fullPage?: FullPageScreenshotWdioOptions;
135140
baselineOverride?: BaselineOverrideIn;
136141
};
137142

@@ -165,7 +170,7 @@ export default class SauceVisualService implements Services.ServiceInstance {
165170
captureDom: boolean | undefined;
166171
clipSelector: string | undefined;
167172
clipElement: WdioElement | undefined;
168-
fullPage?: FullPageScreenshotOptions;
173+
fullPage?: FullPageScreenshotWdioOptions;
169174
apiClient: VisualApi;
170175
baselineOverride?: BaselineOverrideIn;
171176

@@ -442,7 +447,11 @@ export default class SauceVisualService implements Services.ServiceInstance {
442447
options.diffingMethod || this.diffingMethod || DiffingMethod.Balanced,
443448
suiteName: this.test?.parent,
444449
testName: this.test?.title,
445-
fullPageConfig: getFullPageConfig(this.fullPage, options.fullPage),
450+
fullPageConfig: await getFullPageConfig<WdioElement>(
451+
this.fullPage,
452+
options.fullPage,
453+
(el) => el.elementId,
454+
),
446455
baselineOverride: options.baselineOverride || this.baselineOverride,
447456
});
448457
uploadedDiffIds.push(...result.diffs.nodes.flatMap((diff) => diff.id));

visual-js/visual-wdio/src/guarded-types.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { type } from 'arktype';
2-
import { makeValidate, RegionIn } from '@saucelabs/visual';
2+
import {
3+
FullPageScreenshotOptions,
4+
makeValidate,
5+
RegionIn,
6+
} from '@saucelabs/visual';
37

48
export type WdioElement = WebdriverIO.Element;
59

10+
export type FullPageScreenshotWdioOptions =
11+
FullPageScreenshotOptions<WdioElement>;
12+
613
const wdioElementType = type({
714
elementId: 'string',
815
selector: 'string',

visual-js/visual/src/graphql/__generated__/graphql.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

visual-js/visual/src/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RegionIn } from './graphql/__generated__/graphql';
22
import { SelectiveRegionOptions } from './common/selective-region';
33
import { SauceRegion } from './common/regions';
44

5-
export type FullPageScreenshotOptions =
5+
export type FullPageScreenshotOptions<T> =
66
| boolean
77
| {
88
/**
@@ -34,6 +34,10 @@ export type FullPageScreenshotOptions =
3434
* Limit the number of screenshots taken for scrolling and stitching.
3535
*/
3636
scrollLimit?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
37+
/**
38+
* Element used for scrolling (available only in native apps)
39+
*/
40+
scrollElement?: T | Promise<T>;
3741
};
3842

3943
export type Ignorable<T> = T | T[] | Promise<T> | Promise<T[]> | RegionIn;

visual-js/visual/src/utils.spec.ts

+65-37
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { describe, expect, test } from '@jest/globals';
22
import { getFullPageConfig, parseRegionsForAPI } from './utils';
3-
import { FullPageConfigIn, RegionIn } from './graphql/__generated__/graphql';
3+
import { RegionIn } from './graphql/__generated__/graphql';
4+
import { FullPageScreenshotOptions } from './types';
45

5-
const configDelay: FullPageConfigIn = {
6+
type MockElement = { elementId: string };
7+
8+
const configDelay: FullPageScreenshotOptions<MockElement> = {
69
delayAfterScrollMs: 1500,
710
};
811

9-
const configDelayBig: FullPageConfigIn = {
12+
const configDelayBig: FullPageScreenshotOptions<MockElement> = {
1013
delayAfterScrollMs: 5000,
1114
};
1215

@@ -23,66 +26,91 @@ const resolveForTest = async (itemPromise: string | Promise<RegionIn>) => {
2326
describe('utils', () => {
2427
describe('getFullPageConfig', () => {
2528
describe('returns undefined', () => {
26-
test('when main is true and local is false', () => {
27-
expect(getFullPageConfig(true, false)).toBeUndefined();
29+
test('when main is true and local is false', async () => {
30+
expect(await getFullPageConfig(true, false)).toBeUndefined();
2831
});
29-
test('when main is false and local is false', () => {
30-
expect(getFullPageConfig(false, false)).toBeUndefined();
32+
test('when main is false and local is false', async () => {
33+
expect(await getFullPageConfig(false, false)).toBeUndefined();
3134
});
32-
test('when main is false and local is false', () => {
33-
expect(getFullPageConfig(false, false)).toBeUndefined();
35+
test('when main is false and local is false', async () => {
36+
expect(await getFullPageConfig(false, false)).toBeUndefined();
3437
});
35-
test('when main is object and local is false', () => {
36-
expect(getFullPageConfig(configDelay, false)).toBeUndefined();
38+
test('when main is object and local is false', async () => {
39+
expect(await getFullPageConfig(configDelay, false)).toBeUndefined();
3740
});
38-
test('when main is undefined and local is false', () => {
39-
expect(getFullPageConfig(undefined, false)).toBeUndefined();
41+
test('when main is undefined and local is false', async () => {
42+
expect(await getFullPageConfig(undefined, false)).toBeUndefined();
4043
});
41-
test('when main is undefined and local is undefined', () => {
42-
expect(getFullPageConfig(undefined, undefined)).toBeUndefined();
44+
test('when main is undefined and local is undefined', async () => {
45+
expect(await getFullPageConfig(undefined, undefined)).toBeUndefined();
4346
});
44-
test('when main is false and local is undefined', () => {
45-
expect(getFullPageConfig(false, undefined)).toBeUndefined();
47+
test('when main is false and local is undefined', async () => {
48+
expect(await getFullPageConfig(false, undefined)).toBeUndefined();
4649
});
4750
});
4851
describe('returns empty config', () => {
49-
test('when main is true and local is true', () => {
50-
expect(getFullPageConfig(true, undefined)).toEqual({});
52+
test('when main is true and local is true', async () => {
53+
expect(await getFullPageConfig(true, undefined)).toEqual({});
5154
});
52-
test('when main is false and local is true', () => {
53-
expect(getFullPageConfig(true, undefined)).toEqual({});
55+
test('when main is false and local is true', async () => {
56+
expect(await getFullPageConfig(true, undefined)).toEqual({});
5457
});
55-
test('when main is undefined and local is true', () => {
56-
expect(getFullPageConfig(true, undefined)).toEqual({});
58+
test('when main is undefined and local is true', async () => {
59+
expect(await getFullPageConfig(true, undefined)).toEqual({});
5760
});
58-
test('when main is true and local is undefined', () => {
59-
expect(getFullPageConfig(true, undefined)).toEqual({});
61+
test('when main is true and local is undefined', async () => {
62+
expect(await getFullPageConfig(true, undefined)).toEqual({});
6063
});
6164
});
6265
describe('returns config', () => {
63-
test('when main is config and local is true', () => {
64-
expect(getFullPageConfig(configDelay, true)).toEqual(configDelay);
66+
test('when main is config and local is true', async () => {
67+
expect(await getFullPageConfig(configDelay, true)).toEqual(configDelay);
6568
});
66-
test('when main is true and local is config', () => {
67-
expect(getFullPageConfig(true, configDelay)).toEqual(configDelay);
69+
test('when main is true and local is config', async () => {
70+
expect(await getFullPageConfig(true, configDelay)).toEqual(configDelay);
6871
});
69-
test('when main is false and local is config', () => {
70-
expect(getFullPageConfig(true, configDelay)).toEqual(configDelay);
72+
test('when main is false and local is config', async () => {
73+
expect(await getFullPageConfig(true, configDelay)).toEqual(configDelay);
7174
});
72-
test('when main is config and local is config', () => {
73-
expect(getFullPageConfig(configDelay, configDelay)).toEqual(
75+
test('when main is config and local is config', async () => {
76+
expect(await getFullPageConfig(configDelay, configDelay)).toEqual(
7477
configDelay,
7578
);
7679
});
77-
test('and local overwrites main config', () => {
78-
expect(getFullPageConfig(configDelay, configDelayBig)).toEqual(
80+
test('and local overwrites main config', async () => {
81+
expect(await getFullPageConfig(configDelay, configDelayBig)).toEqual(
7982
configDelayBig,
8083
);
8184
});
82-
test('with merged local and main config', () => {
85+
test('with merged local and main config', async () => {
8386
const main = { delayAfterScrollMs: 500 };
8487
const local = { disableCSSAnimation: false };
85-
expect(getFullPageConfig(main, local)).toEqual({ ...main, ...local });
88+
expect(await getFullPageConfig(main, local)).toEqual({
89+
...main,
90+
...local,
91+
});
92+
});
93+
test('with scrollElement when scrollElement is a promise', async () => {
94+
const elementId = 'elementId';
95+
const main = {};
96+
const local = {
97+
scrollElement: Promise.resolve({ elementId: elementId }),
98+
};
99+
expect(
100+
await getFullPageConfig(main, local, (el) => el.elementId),
101+
).toEqual({
102+
scrollElement: elementId,
103+
});
104+
});
105+
test('with scrollElement when scrollElement is an object', async () => {
106+
const elementId = 'elementId';
107+
const main = { scrollElement: { elementId: elementId } };
108+
const local = {};
109+
expect(
110+
await getFullPageConfig(main, local, (el) => el.elementId),
111+
).toEqual({
112+
scrollElement: elementId,
113+
});
86114
});
87115
});
88116
});

visual-js/visual/src/utils.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,34 @@ import {
66
InputMaybe,
77
RegionIn,
88
} from './graphql/__generated__/graphql';
9-
import { RegionType, VisualEnvOpts } from './types';
9+
import { FullPageScreenshotOptions, RegionType, VisualEnvOpts } from './types';
1010
import { selectiveRegionOptionsToDiffingOptions } from './common/selective-region';
1111
import { getApi } from './common/api';
1212
import fs from 'fs/promises';
1313
import * as os from 'node:os';
1414
import { SauceRegion } from './common/regions';
1515

16-
export const getFullPageConfig: (
17-
main?: FullPageConfigIn | boolean,
18-
local?: FullPageConfigIn | boolean,
19-
) => FullPageConfigIn | undefined = (main, local) => {
16+
export const getFullPageConfig: <T>(
17+
main?: FullPageScreenshotOptions<T> | boolean,
18+
local?: FullPageScreenshotOptions<T> | boolean,
19+
getId?: (el: T) => Promise<string> | string,
20+
) => Promise<FullPageConfigIn | undefined> = async (main, local, getId) => {
2021
const isNoConfig = !main && !local;
2122
const isLocalOff = local === false;
2223

2324
if (isNoConfig || isLocalOff) {
2425
return;
2526
}
2627

27-
const globalCfg = typeof main === 'object' ? main : {};
28-
const localCfg = typeof local === 'object' ? local : {};
29-
return { ...globalCfg, ...localCfg };
28+
const globalCfg: typeof main = typeof main === 'object' ? main : {};
29+
const localCfg: typeof main = typeof local === 'object' ? local : {};
30+
const { scrollElement, ...rest } = { ...globalCfg, ...localCfg };
31+
const result: FullPageConfigIn = rest;
32+
if (scrollElement && getId) {
33+
result.scrollElement = await getId(await scrollElement);
34+
}
35+
36+
return result;
3037
};
3138

3239
export const isSkipMode = (): boolean => {

0 commit comments

Comments
 (0)