diff --git a/packages/client/test/client.test.js b/packages/client/test/client.test.js index 6227423ae..7d7259d98 100644 --- a/packages/client/test/client.test.js +++ b/packages/client/test/client.test.js @@ -2293,6 +2293,25 @@ describe('PercyClient', () => { })).toBeRejectedWithError('sha, filepath or content should be present in tiles object'); }); }); + + describe('when labels are provided on a POA comparison', () => { + beforeEach(async () => { + await client.sendComparison(123, { + name: 'test snapshot name', + tag: { name: 'test tag' }, + tiles: [{ content: base64encode('tile') }], + labels: 'qa, smoke,release' + }); + }); + + it('forwards labels as tags onto the snapshot', () => { + expect(api.requests['/builds/123/snapshots'][0].body.data.attributes.tags).toEqual([ + { id: null, name: 'qa' }, + { id: null, name: 'smoke' }, + { id: null, name: 'release' } + ]); + }); + }); }); describe('#tokenType', () => { diff --git a/packages/core/test/api.test.js b/packages/core/test/api.test.js index ad6d67962..0cc7fedf0 100644 --- a/packages/core/test/api.test.js +++ b/packages/core/test/api.test.js @@ -496,6 +496,61 @@ describe('API Server', () => { expect(percy.upload).toHaveBeenCalledOnceWith({ sync: true }, jasmine.objectContaining({}), 'automate'); }); + it('has a /automateScreenshot endpoint that propagates labels through to #upload()', async () => { + let resolve, test = new Promise(r => (resolve = r)); + spyOn(percy, 'upload').and.returnValue(test); + // Simulate what the real WebdriverUtils.captureScreenshot does: + // it must copy options.labels onto the returned comparisonData. + let captureScreenshotSpy = spyOn(WebdriverUtils, 'captureScreenshot').and.callFake(({ options }) => { + return Promise.resolve({ + name: 'Snapshot name', + tag: { name: 'tag-1' }, + tiles: [], + metadata: {}, + sync: options.sync, + testCase: options.testCase, + labels: options.labels, + thTestCaseExecutionId: options.thTestCaseExecutionId + }); + }); + + await percy.start(); + + await expectAsync(request('/percy/automateScreenshot', { + body: { + name: 'Snapshot name', + client_info: 'client', + environment_info: 'environment', + options: { + labels: 'qa,smoke', + testCase: 'tc-1', + thTestCaseExecutionId: 'exec-99' + } + }, + method: 'post' + })).toBeResolvedTo({ success: true }); + + expect(captureScreenshotSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ + options: jasmine.objectContaining({ + labels: 'qa,smoke', + testCase: 'tc-1', + thTestCaseExecutionId: 'exec-99' + }) + })); + + expect(percy.upload).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + labels: 'qa,smoke', + testCase: 'tc-1', + thTestCaseExecutionId: 'exec-99' + }), + null, + 'automate' + ); + + resolve(); + }); + // Cross-consumer drain canary for /percy/automateScreenshot. Mirrors the // maestro-screenshot and /comparison canaries elsewhere in this file. See // docs/solutions/best-practices/2026-05-20-maestro-sync-promise-bug-investigation.md. diff --git a/packages/webdriver-utils/src/index.js b/packages/webdriver-utils/src/index.js index 40655fa8e..5b0b9a8b4 100644 --- a/packages/webdriver-utils/src/index.js +++ b/packages/webdriver-utils/src/index.js @@ -39,6 +39,7 @@ export default class WebdriverUtils { comparisonData.metadata.cliScreenshotEndTime = Date.now(); comparisonData.sync = options.sync; comparisonData.testCase = options.testCase; + comparisonData.labels = options.labels; comparisonData.thTestCaseExecutionId = options.thTestCaseExecutionId; log.debug(`[${snapshotName}] : Comparison Data: ${JSON.stringify(comparisonData)}`); return comparisonData; diff --git a/packages/webdriver-utils/test/index.test.js b/packages/webdriver-utils/test/index.test.js new file mode 100644 index 000000000..2ec0da2e0 --- /dev/null +++ b/packages/webdriver-utils/test/index.test.js @@ -0,0 +1,104 @@ +import WebdriverUtils from '../src/index.js'; +import ProviderResolver from '../src/providers/providerResolver.js'; +import PlaywrightProvider from '../src/providers/playwrightProvider.js'; + +describe('WebdriverUtils.captureScreenshot', () => { + let providerStub; + let baseArgs; + let providerResponse; + + beforeEach(() => { + providerResponse = { + name: 'snap', + tag: { name: 'tag-1' }, + tiles: [], + metadata: {} + }; + + providerStub = { + createDriver: jasmine.createSpy('createDriver').and.resolveTo(), + screenshot: jasmine.createSpy('screenshot').and.callFake(() => Promise.resolve({ ...providerResponse })) + }; + + spyOn(ProviderResolver, 'resolve').and.returnValue(providerStub); + + baseArgs = { + sessionId: '1234', + commandExecutorUrl: 'https://localhost/command-executor', + capabilities: {}, + sessionCapabilities: {}, + framework: null, + snapshotName: 'snap', + clientInfo: 'client', + environmentInfo: 'env', + options: {}, + buildInfo: { id: '123' } + }; + }); + + it('forwards labels from options onto comparisonData for POA snapshots', async () => { + baseArgs.options = { + sync: false, + testCase: 'tc', + labels: 'label1,label2', + thTestCaseExecutionId: 'exec-1' + }; + + const result = await WebdriverUtils.captureScreenshot(baseArgs); + + expect(result.labels).toEqual('label1,label2'); + expect(result.testCase).toEqual('tc'); + expect(result.thTestCaseExecutionId).toEqual('exec-1'); + expect(result.sync).toEqual(false); + }); + + it('sets labels to undefined when not provided in options', async () => { + baseArgs.options = { testCase: 'tc' }; + + const result = await WebdriverUtils.captureScreenshot(baseArgs); + + expect(result.labels).toBeUndefined(); + expect(result.testCase).toEqual('tc'); + }); + + it('does not lose labels even when provider response has none', async () => { + baseArgs.options = { labels: 'only-label' }; + + const result = await WebdriverUtils.captureScreenshot(baseArgs); + + expect(result.labels).toEqual('only-label'); + }); + + it('forwards labels through the playwright provider too', async () => { + spyOn(PlaywrightProvider.prototype, 'createDriver').and.resolveTo(); + spyOn(PlaywrightProvider.prototype, 'screenshot').and.resolveTo({ + name: 'snap', tag: { name: 'pw' }, tiles: [], metadata: {} + }); + + baseArgs.framework = 'playwright'; + baseArgs.options = { labels: 'pw-label' }; + + const result = await WebdriverUtils.captureScreenshot(baseArgs); + + expect(result.labels).toEqual('pw-label'); + }); + + it('re-throws errors from the provider without setting labels', async () => { + providerStub.screenshot.and.rejectWith(new Error('boom')); + baseArgs.options = { labels: 'x' }; + + await expectAsync(WebdriverUtils.captureScreenshot(baseArgs)) + .toBeRejectedWithError('boom'); + }); + + it('falls back to default options and buildInfo when caller omits them', async () => { + const { options, buildInfo, ...argsWithoutDefaults } = baseArgs; + + const result = await WebdriverUtils.captureScreenshot(argsWithoutDefaults); + + expect(result.labels).toBeUndefined(); + expect(result.testCase).toBeUndefined(); + expect(result.thTestCaseExecutionId).toBeUndefined(); + expect(providerStub.screenshot).toHaveBeenCalledWith('snap', {}); + }); +}); diff --git a/test/regression/pages/comprehensive.html b/test/regression/pages/comprehensive.html deleted file mode 100644 index 66f495e7f..000000000 --- a/test/regression/pages/comprehensive.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - Comprehensive Regression Test - - - - -

Comprehensive Smoke Test

-

This page combines all major asset discovery features into a single snapshot.

- - -
-
Basic HTML + CSS
-

Static text content with bold and italic formatting.

-
- - -
-
Image
- Test logo -
- - -
-
Web Font
-

This text uses a custom web font loaded via @font-face.

-
- - -
-
Shadow DOM Component
- -

This content is slotted into a shadow DOM component.

-
-
- - -
-
Canvas
- -
- - -
-
Video Poster
- -
- - -
-
Base64 Inline Image
- Inline base64 -
- - -
-
Pseudo-class CSS
- - -
- - -
-
SVG Image
- SVG icon -
- - - - -