Skip to content

Commit

Permalink
add scrollElement fps option for native apps (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
paweltomaszewskisaucelabs authored Oct 4, 2024
1 parent 93c7f19 commit 8572e1b
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 75 deletions.
7 changes: 7 additions & 0 deletions visual-js/.changeset/lucky-ladybugs-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@saucelabs/nightwatch-sauce-visual-service": minor
"@saucelabs/wdio-sauce-visual-service": minor
"@saucelabs/visual": minor
---

scrollElement native fps
8 changes: 6 additions & 2 deletions visual-js/visual-nightwatch/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ WORKDIR app

COPY tsconfig.prod.json tsconfig.json package.json ./

COPY ./visual/src ./visual/src
COPY ./visual/package.json ./visual/tsconfig.json ./visual/
COPY ./visual-nightwatch/src ./visual-nightwatch/src
COPY ./visual-nightwatch/package.json ./visual-nightwatch/tsconfig.json ./visual-nightwatch
COPY ./visual-nightwatch/package.json ./visual-nightwatch/tsconfig.json ./visual-nightwatch/

RUN npm install --save-dev @tsconfig/node18
RUN npm install --workspace=visual
RUN npm run build --workspace=visual
RUN npm install --workspace=visual-nightwatch
RUN npm run build --workspace=visual-nightwatch

Expand All @@ -22,4 +26,4 @@ WORKDIR integration-tests

RUN npm install

ENTRYPOINT ["npm", "run", "external"]
ENTRYPOINT ["npm", "run", "external"]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
} from '@saucelabs/visual';
import { getMetaInfo, getVisualApi } from '../../utils/api';
import { VISUAL_BUILD_ID_KEY } from '../../utils/constants';
import { NightwatchAPI, NightwatchCustomCommandsModel } from 'nightwatch';
import {
NightwatchAPI,
NightwatchCustomCommandsModel,
ScopedElement,
} from 'nightwatch';
import { CheckOptions, NightwatchIgnorable, RunnerSettings } from '../../types';
import type { Runnable } from 'mocha';

Expand Down Expand Up @@ -148,7 +152,11 @@ class SauceVisualCheck implements NightwatchCustomCommandsModel {
sessionMetadata: metaInfo,
suiteName,
testName,
fullPageConfig: getFullPageConfig(fullPage, options.fullPage),
fullPageConfig: await getFullPageConfig<ScopedElement>(
fullPage,
options.fullPage,
async (el) => await el.getId(),
),
clipElement:
(await options.clipElement?.getId()) ?? clipElementFromClipSelector,
captureDom: options.captureDom ?? globalCaptureDom,
Expand Down
2 changes: 1 addition & 1 deletion visual-js/visual-nightwatch/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface CheckOptions {
ignore?: NightwatchIgnorable[];
regions?: RegionType<ElementType>[];
diffingMethod?: DiffingMethod;
fullPage?: FullPageScreenshotOptions;
fullPage?: FullPageScreenshotOptions<ScopedElement>;
/**
* Whether we should take a snapshot of the DOM to compare with as a part of the diffing process.
*/
Expand Down
23 changes: 5 additions & 18 deletions visual-js/visual-wdio/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,14 @@ FROM node:18 AS runner

WORKDIR app

COPY tsconfig.prod.json .
COPY tsconfig.json .
COPY package.json .
RUN corepack enable

COPY ./visual-wdio/src ./visual-wdio/src
COPY ./visual-wdio/package.json ./visual-wdio/package.json
COPY ./visual-wdio/tsconfig.json ./visual-wdio/tsconfig.json
COPY ./visual-wdio/tsconfig.build.json ./visual-wdio/tsconfig.build.json
COPY . ./

RUN npm install --workspace=visual-wdio
RUN npm run build --workspace=visual-wdio
RUN yarn install && npm run build --workspaces --if-present

COPY ./visual-wdio/integration-tests/configs ./integration-tests/configs
COPY ./visual-wdio/integration-tests/helpers ./integration-tests/helpers
COPY ./visual-wdio/integration-tests/pages ./integration-tests/pages
COPY ./visual-wdio/integration-tests/specs ./integration-tests/specs

COPY ./visual-wdio/integration-tests/package.json ./integration-tests/package.json

WORKDIR integration-tests
WORKDIR ./visual-wdio/integration-tests

RUN npm install

ENTRYPOINT ["npm", "run", "login-test"]
ENTRYPOINT ["npm", "run", "login-test"]
19 changes: 14 additions & 5 deletions visual-js/visual-wdio/src/SauceVisualService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ import {

import logger from '@wdio/logger';
import chalk from 'chalk';
import { Ignorable, isWdioElement, WdioElement } from './guarded-types.js';
import {
FullPageScreenshotWdioOptions,
Ignorable,
isWdioElement,
WdioElement,
} from './guarded-types.js';
import { backOff } from 'exponential-backoff';
import type { Test } from '@wdio/types/build/Frameworks';

Expand Down Expand Up @@ -95,7 +100,7 @@ export type SauceVisualServiceOptions = {
clipSelector?: string;
clipElement?: WdioElement;
region?: SauceRegion;
fullPage?: FullPageScreenshotOptions;
fullPage?: FullPageScreenshotWdioOptions;
baselineOverride?: BaselineOverrideIn;
};

Expand Down Expand Up @@ -131,7 +136,7 @@ export type CheckOptions = {
captureDom?: boolean;
diffingMethod?: DiffingMethod;
disable?: (keyof DiffingOptionsIn)[];
fullPage?: FullPageScreenshotOptions;
fullPage?: FullPageScreenshotWdioOptions;
baselineOverride?: BaselineOverrideIn;
};

Expand Down Expand Up @@ -165,7 +170,7 @@ export default class SauceVisualService implements Services.ServiceInstance {
captureDom: boolean | undefined;
clipSelector: string | undefined;
clipElement: WdioElement | undefined;
fullPage?: FullPageScreenshotOptions;
fullPage?: FullPageScreenshotWdioOptions;
apiClient: VisualApi;
baselineOverride?: BaselineOverrideIn;

Expand Down Expand Up @@ -442,7 +447,11 @@ export default class SauceVisualService implements Services.ServiceInstance {
options.diffingMethod || this.diffingMethod || DiffingMethod.Balanced,
suiteName: this.test?.parent,
testName: this.test?.title,
fullPageConfig: getFullPageConfig(this.fullPage, options.fullPage),
fullPageConfig: await getFullPageConfig<WdioElement>(
this.fullPage,
options.fullPage,
(el) => el.elementId,
),
baselineOverride: options.baselineOverride || this.baselineOverride,
});
uploadedDiffIds.push(...result.diffs.nodes.flatMap((diff) => diff.id));
Expand Down
9 changes: 8 additions & 1 deletion visual-js/visual-wdio/src/guarded-types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { type } from 'arktype';
import { makeValidate, RegionIn } from '@saucelabs/visual';
import {
FullPageScreenshotOptions,
makeValidate,
RegionIn,
} from '@saucelabs/visual';

export type WdioElement = WebdriverIO.Element;

export type FullPageScreenshotWdioOptions =
FullPageScreenshotOptions<WdioElement>;

const wdioElementType = type({
elementId: 'string',
selector: 'string',
Expand Down
2 changes: 2 additions & 0 deletions visual-js/visual/src/graphql/__generated__/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion visual-js/visual/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { RegionIn } from './graphql/__generated__/graphql';
import { SelectiveRegionOptions } from './common/selective-region';
import { SauceRegion } from './common/regions';

export type FullPageScreenshotOptions =
export type FullPageScreenshotOptions<T> =
| boolean
| {
/**
Expand Down Expand Up @@ -34,6 +34,10 @@ export type FullPageScreenshotOptions =
* Limit the number of screenshots taken for scrolling and stitching.
*/
scrollLimit?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
/**
* Element used for scrolling (available only in native apps)
*/
scrollElement?: T | Promise<T>;
};

export type Ignorable<T> = T | T[] | Promise<T> | Promise<T[]> | RegionIn;
Expand Down
102 changes: 65 additions & 37 deletions visual-js/visual/src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { describe, expect, test } from '@jest/globals';
import { getFullPageConfig, parseRegionsForAPI } from './utils';
import { FullPageConfigIn, RegionIn } from './graphql/__generated__/graphql';
import { RegionIn } from './graphql/__generated__/graphql';
import { FullPageScreenshotOptions } from './types';

const configDelay: FullPageConfigIn = {
type MockElement = { elementId: string };

const configDelay: FullPageScreenshotOptions<MockElement> = {
delayAfterScrollMs: 1500,
};

const configDelayBig: FullPageConfigIn = {
const configDelayBig: FullPageScreenshotOptions<MockElement> = {
delayAfterScrollMs: 5000,
};

Expand All @@ -23,66 +26,91 @@ const resolveForTest = async (itemPromise: string | Promise<RegionIn>) => {
describe('utils', () => {
describe('getFullPageConfig', () => {
describe('returns undefined', () => {
test('when main is true and local is false', () => {
expect(getFullPageConfig(true, false)).toBeUndefined();
test('when main is true and local is false', async () => {
expect(await getFullPageConfig(true, false)).toBeUndefined();
});
test('when main is false and local is false', () => {
expect(getFullPageConfig(false, false)).toBeUndefined();
test('when main is false and local is false', async () => {
expect(await getFullPageConfig(false, false)).toBeUndefined();
});
test('when main is false and local is false', () => {
expect(getFullPageConfig(false, false)).toBeUndefined();
test('when main is false and local is false', async () => {
expect(await getFullPageConfig(false, false)).toBeUndefined();
});
test('when main is object and local is false', () => {
expect(getFullPageConfig(configDelay, false)).toBeUndefined();
test('when main is object and local is false', async () => {
expect(await getFullPageConfig(configDelay, false)).toBeUndefined();
});
test('when main is undefined and local is false', () => {
expect(getFullPageConfig(undefined, false)).toBeUndefined();
test('when main is undefined and local is false', async () => {
expect(await getFullPageConfig(undefined, false)).toBeUndefined();
});
test('when main is undefined and local is undefined', () => {
expect(getFullPageConfig(undefined, undefined)).toBeUndefined();
test('when main is undefined and local is undefined', async () => {
expect(await getFullPageConfig(undefined, undefined)).toBeUndefined();
});
test('when main is false and local is undefined', () => {
expect(getFullPageConfig(false, undefined)).toBeUndefined();
test('when main is false and local is undefined', async () => {
expect(await getFullPageConfig(false, undefined)).toBeUndefined();
});
});
describe('returns empty config', () => {
test('when main is true and local is true', () => {
expect(getFullPageConfig(true, undefined)).toEqual({});
test('when main is true and local is true', async () => {
expect(await getFullPageConfig(true, undefined)).toEqual({});
});
test('when main is false and local is true', () => {
expect(getFullPageConfig(true, undefined)).toEqual({});
test('when main is false and local is true', async () => {
expect(await getFullPageConfig(true, undefined)).toEqual({});
});
test('when main is undefined and local is true', () => {
expect(getFullPageConfig(true, undefined)).toEqual({});
test('when main is undefined and local is true', async () => {
expect(await getFullPageConfig(true, undefined)).toEqual({});
});
test('when main is true and local is undefined', () => {
expect(getFullPageConfig(true, undefined)).toEqual({});
test('when main is true and local is undefined', async () => {
expect(await getFullPageConfig(true, undefined)).toEqual({});
});
});
describe('returns config', () => {
test('when main is config and local is true', () => {
expect(getFullPageConfig(configDelay, true)).toEqual(configDelay);
test('when main is config and local is true', async () => {
expect(await getFullPageConfig(configDelay, true)).toEqual(configDelay);
});
test('when main is true and local is config', () => {
expect(getFullPageConfig(true, configDelay)).toEqual(configDelay);
test('when main is true and local is config', async () => {
expect(await getFullPageConfig(true, configDelay)).toEqual(configDelay);
});
test('when main is false and local is config', () => {
expect(getFullPageConfig(true, configDelay)).toEqual(configDelay);
test('when main is false and local is config', async () => {
expect(await getFullPageConfig(true, configDelay)).toEqual(configDelay);
});
test('when main is config and local is config', () => {
expect(getFullPageConfig(configDelay, configDelay)).toEqual(
test('when main is config and local is config', async () => {
expect(await getFullPageConfig(configDelay, configDelay)).toEqual(
configDelay,
);
});
test('and local overwrites main config', () => {
expect(getFullPageConfig(configDelay, configDelayBig)).toEqual(
test('and local overwrites main config', async () => {
expect(await getFullPageConfig(configDelay, configDelayBig)).toEqual(
configDelayBig,
);
});
test('with merged local and main config', () => {
test('with merged local and main config', async () => {
const main = { delayAfterScrollMs: 500 };
const local = { disableCSSAnimation: false };
expect(getFullPageConfig(main, local)).toEqual({ ...main, ...local });
expect(await getFullPageConfig(main, local)).toEqual({
...main,
...local,
});
});
test('with scrollElement when scrollElement is a promise', async () => {
const elementId = 'elementId';
const main = {};
const local = {
scrollElement: Promise.resolve({ elementId: elementId }),
};
expect(
await getFullPageConfig(main, local, (el) => el.elementId),
).toEqual({
scrollElement: elementId,
});
});
test('with scrollElement when scrollElement is an object', async () => {
const elementId = 'elementId';
const main = { scrollElement: { elementId: elementId } };
const local = {};
expect(
await getFullPageConfig(main, local, (el) => el.elementId),
).toEqual({
scrollElement: elementId,
});
});
});
});
Expand Down
23 changes: 15 additions & 8 deletions visual-js/visual/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,34 @@ import {
InputMaybe,
RegionIn,
} from './graphql/__generated__/graphql';
import { RegionType, VisualEnvOpts } from './types';
import { FullPageScreenshotOptions, RegionType, VisualEnvOpts } from './types';
import { selectiveRegionOptionsToDiffingOptions } from './common/selective-region';
import { getApi } from './common/api';
import fs from 'fs/promises';
import * as os from 'node:os';
import { SauceRegion } from './common/regions';

export const getFullPageConfig: (
main?: FullPageConfigIn | boolean,
local?: FullPageConfigIn | boolean,
) => FullPageConfigIn | undefined = (main, local) => {
export const getFullPageConfig: <T>(
main?: FullPageScreenshotOptions<T> | boolean,
local?: FullPageScreenshotOptions<T> | boolean,
getId?: (el: T) => Promise<string> | string,
) => Promise<FullPageConfigIn | undefined> = async (main, local, getId) => {
const isNoConfig = !main && !local;
const isLocalOff = local === false;

if (isNoConfig || isLocalOff) {
return;
}

const globalCfg = typeof main === 'object' ? main : {};
const localCfg = typeof local === 'object' ? local : {};
return { ...globalCfg, ...localCfg };
const globalCfg: typeof main = typeof main === 'object' ? main : {};
const localCfg: typeof main = typeof local === 'object' ? local : {};
const { scrollElement, ...rest } = { ...globalCfg, ...localCfg };
const result: FullPageConfigIn = rest;
if (scrollElement && getId) {
result.scrollElement = await getId(await scrollElement);
}

return result;
};

export const isSkipMode = (): boolean => {
Expand Down

0 comments on commit 8572e1b

Please sign in to comment.