| import {assert} from '@open-wc/testing'; |
| |
| import './test-setup'; |
| import { |
| ResultDbV1Client, |
| TestStatus, |
| TestVariantStatus, |
| } from '../resultdb-client'; |
| import {DATA_SYMBOL} from '../checks-fetcher'; |
| import { |
| installChecksResult, |
| replaceTextArtifacts, |
| parseMarkdown, |
| } from '../checks-result'; |
| import {FaultAttribute} from '../failure-analysis'; |
| |
| function sanitizedAttr(attr: string) { |
| return `rel="noopener" target="_blank" ${attr}`; |
| } |
| |
| suite('checks-result tests', () => { |
| let sandbox: sinon.SinonSandbox; |
| const defaultMetaAttr = 'alt="chromeChecksMetadataTagDefault"'; |
| const greenMetaAttr = 'alt="chromeChecksMetadataTagGreen"'; |
| const redMetaAttr = 'alt="chromeChecksMetadataTagRed"'; |
| |
| setup(() => { |
| sandbox = sinon.createSandbox(); |
| sandbox |
| .stub(ResultDbV1Client.prototype, 'getAuthorizationHeader') |
| .returns(Promise.resolve({authorization: 'accessToken'})); |
| }); |
| |
| teardown(() => { |
| sandbox.restore(); |
| }); |
| |
| test('installChecksResult formats the CheckResult row', async () => { |
| const message = 'Run #1: unexpectedly failed.'; |
| |
| /* |
| * Recreate Gerrit endpoint DOM. |
| * |
| * <container> |
| * <shadow> |
| * <gr-endpoint-decorator name="check-result-expanded"> |
| * <messageDiv class="message"> |
| * message text |
| * </messageDiv> |
| * </gr-endpoint-decorator> |
| * </shadow> |
| * </container> |
| * |
| */ |
| const container = document.createElement('div'); |
| const shadow = container.attachShadow({mode: 'open'}); |
| const element = document.createElement('div'); |
| (element as any).result = {message}; |
| shadow.appendChild(element); |
| |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = 'message'; |
| messageDiv.textContent = message; |
| element.appendChild(messageDiv); |
| |
| await installChecksResult(element); |
| |
| // Check element is styled. |
| const style = (element.getRootNode() as any).querySelector( |
| 'style' |
| ).textContent; |
| assert.isTrue(style.includes('word-break: break-all;')); |
| assert.isTrue(style.includes('white-space: pre-wrap;')); |
| |
| // Check original messageDiv removed. |
| assert.notStrictEqual(messageDiv.parentNode, container); |
| |
| // Check element's data is used to format the row. |
| const fetchStub = sandbox.stub(window, 'fetch'); |
| fetchStub.returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => 'foo test failed!', |
| } as Response) |
| ); |
| |
| const artifactStub = sandbox.stub( |
| ResultDbV1Client.prototype, |
| 'listArtifacts' |
| ); |
| artifactStub.returns( |
| Promise.resolve({ |
| artifacts: [ |
| { |
| name: 'invocations/123/foo/artifacts/baz', |
| artifactId: 'foo', |
| fetchUrl: 'https://foo.com', |
| fetchUrlExpiration: '2017-12-15T01:30:15.05Z', |
| sizeBytes: 10, |
| }, |
| ], |
| }) |
| ); |
| |
| (element as any).result[DATA_SYMBOL] = { |
| variant: {variant: {def: {builder: 'baz', os: 'foo-OS'}}}, |
| testResults: [ |
| { |
| result: { |
| name: 'invocations/123/foo', |
| status: TestStatus.FAIL, |
| summaryHtml: 'hello <text-artifact artifact-id="foo" />', |
| expected: false, |
| }, |
| }, |
| { |
| result: { |
| status: TestStatus.PASS, |
| summaryHtml: '', |
| expected: true, |
| }, |
| }, |
| ], |
| plugin: { |
| checks: () => { |
| return { |
| updateResult: (_1: any, _2: any) => {}, |
| }; |
| }, |
| }, |
| rdbHost: 'host', |
| }; |
| await installChecksResult(element); |
| assert.strictEqual( |
| element.innerHTML, |
| `<a ${sanitizedAttr( |
| defaultMetaAttr |
| )}><em>builder: baz, os: foo-OS</em></a>` + |
| '<ul>' + |
| '<li>' + |
| '<h3>' + |
| `<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` + |
| `<a ${sanitizedAttr(redMetaAttr)}>unexpectedly failed</a>` + |
| '</h3>' + |
| 'hello foo test failed!' + |
| '</li>' + |
| '<li>' + |
| '<h3>' + |
| `<a ${sanitizedAttr(defaultMetaAttr)}>Run #2: </a>` + |
| `<a ${sanitizedAttr(greenMetaAttr)}>expectedly passed</a>` + |
| '</h3>' + |
| '</li>' + |
| '</ul>' |
| ); |
| |
| // Variants without definitions and artifacts without sizes are processed. |
| artifactStub.returns( |
| Promise.resolve({ |
| artifacts: [ |
| { |
| name: 'invocations/123/baz/artifacts/bar', |
| artifactId: 'baz', |
| fetchUrl: 'https://baz.com', |
| fetchUrlExpiration: '2017-12-15T01:30:15.05Z', |
| sizeBytes: 0, |
| }, |
| ], |
| }) |
| ); |
| |
| (element as any).result[DATA_SYMBOL] = { |
| testResults: [ |
| { |
| result: { |
| name: 'invocations/123/baz', |
| status: TestStatus.FAIL, |
| summaryHtml: 'bye <text-artifact artifact-id="baz" />', |
| }, |
| }, |
| ], |
| plugin: { |
| checks: () => { |
| return { |
| updateResult: (_1: any, _2: any) => {}, |
| }; |
| }, |
| }, |
| rdbHost: 'host', |
| }; |
| await installChecksResult(element); |
| assert.strictEqual( |
| element.innerHTML, |
| '<ul>' + |
| '<li>' + |
| '<h3>' + |
| `<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` + |
| `<a ${sanitizedAttr(redMetaAttr)}>unexpectedly failed</a>` + |
| '</h3>' + |
| `bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` + |
| '</li>' + |
| '</ul>' |
| ); |
| |
| // Exonerations are shown. |
| (element as any).result[DATA_SYMBOL] = { |
| testResults: [ |
| { |
| result: { |
| name: 'invocations/123/baz', |
| status: TestStatus.FAIL, |
| summaryHtml: 'bye <text-artifact artifact-id="baz" />', |
| }, |
| }, |
| ], |
| testExonerations: [ |
| {explanationHtml: '<p>foo is exonerated</p>'}, |
| {explanationHtml: '<p>bar is exonerated</p>'}, |
| ], |
| plugin: { |
| checks: () => { |
| return { |
| updateResult: (_1: any, _2: any) => {}, |
| }; |
| }, |
| }, |
| rdbHost: 'host', |
| variant: { |
| variant: {}, |
| results: [], |
| exonerations: [], |
| status: TestVariantStatus.EXPECTED, |
| variantHash: '123', |
| faultAttribute: { |
| comparisonSnapshot: { |
| sourceBuildId: '8775275012462334081', |
| sourceStartedUnixTimestamp: '1689668613', |
| }, |
| snapshotComparisonFaultAttribution: |
| FaultAttribute.MATCHING_FAILURE_FOUND, |
| testName: 'foo-test', |
| }, |
| }, |
| }; |
| await installChecksResult(element); |
| assert.strictEqual( |
| element.innerHTML, |
| '<br><br>' + |
| '<p>foo is exonerated</p><br><p>bar is exonerated</p>' + |
| '<ul>' + |
| '<li>' + |
| '<h3>' + |
| `<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` + |
| `<a ${sanitizedAttr( |
| redMetaAttr |
| )}>unexpectedly failed as of </a><a rel="noopener" target="_blank" href="https://ci.chromium.org/ui/b/8775275012462334081/test-results?q=ExactID:foo-test+VHash:123">7/18/2023, 8:23:33 AM</a>` + |
| '</h3>' + |
| `bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` + |
| '</li>' + |
| '</ul>' |
| ); |
| |
| // Variant hash excluded from MILO URL if different model was used |
| // for fault attribution. |
| (element as any).result[DATA_SYMBOL] = { |
| testResults: [ |
| { |
| result: { |
| name: 'invocations/123/baz', |
| status: TestStatus.FAIL, |
| summaryHtml: 'bye <text-artifact artifact-id="baz" />', |
| }, |
| }, |
| ], |
| plugin: { |
| checks: () => { |
| return { |
| updateResult: (_1: any, _2: any) => {}, |
| }; |
| }, |
| }, |
| rdbHost: 'host', |
| variant: { |
| variant: {}, |
| results: [], |
| exonerations: [], |
| status: TestVariantStatus.EXPECTED, |
| variantHash: '123', |
| faultAttribute: { |
| comparisonSnapshot: { |
| sourceBuildId: '8775275012462334081', |
| sourceStartedUnixTimestamp: '1689668613', |
| }, |
| snapshotComparisonFaultAttribution: |
| FaultAttribute.MATCHING_FAILURE_FOUND, |
| testName: 'foo-test', |
| diffModelUsed: 'true', |
| }, |
| }, |
| }; |
| await installChecksResult(element); |
| assert.strictEqual( |
| element.innerHTML, |
| '<ul>' + |
| '<li>' + |
| '<h3>' + |
| `<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` + |
| `<a ${sanitizedAttr( |
| redMetaAttr |
| )}>unexpectedly failed as of </a><a rel="noopener" target="_blank" href="https://ci.chromium.org/ui/b/8775275012462334081/test-results?q=ExactID:foo-test">7/18/2023, 8:23:33 AM</a>` + |
| '</h3>' + |
| `bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` + |
| '</li>' + |
| '</ul>' |
| ); |
| }); |
| |
| test('replaceTextArtifacts replaces <text-artifact/>', async () => { |
| const fetchStub = sandbox.stub(window, 'fetch'); |
| fetchStub.withArgs('https://foo.com/?n=50000').returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => 'foo test failed!', |
| } as Response) |
| ); |
| fetchStub.withArgs('https://bar.org/?n=50000').returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => 'bar test passed!', |
| } as Response) |
| ); |
| fetchStub |
| .withArgs('https://baz.org/?n=50000') |
| .returns(Promise.reject(new Error('bad fetch'))); |
| fetchStub.withArgs('https://boo.org/?n=50000').returns( |
| Promise.resolve({ |
| ok: false, |
| status: 500, |
| text: async () => 'server error', |
| } as Response) |
| ); |
| |
| const resArtifacts = [ |
| { |
| artifactId: 'foo', |
| name: 'foo-name', |
| fetchUrl: 'https://foo.com/', |
| sizeBytes: 10, |
| }, |
| { |
| artifactId: 'baz', |
| name: 'baz-name', |
| fetchUrl: 'https://baz.org/', |
| sizeBytes: 10, |
| }, |
| { |
| artifactId: 'boo', |
| name: 'boo-name', |
| fetchUrl: 'https://boo.org/', |
| sizeBytes: 10, |
| }, |
| ]; |
| const invArtifacts = [ |
| { |
| artifactId: 'bar', |
| name: 'bar-name', |
| fetchUrl: 'https://bar.org/', |
| sizeBytes: 10, |
| }, |
| ]; |
| |
| // Replace and fetch artifacts. |
| assert.strictEqual( |
| await replaceTextArtifacts( |
| '<p><text-artifact artifact-id="foo" /></p>' + |
| '<p><text-artifact artifact-id="bar" inv-level /></p>' + |
| '<p><text-artifact artifact-id="baz" /></p>' + |
| '<p><text-artifact artifact-id="boo" /></p>', |
| resArtifacts, |
| invArtifacts |
| ), |
| '<p>foo test failed!</p><p>bar test passed!</p>' + |
| `<p><a ${defaultMetaAttr}>Failed to fetch baz artifact: bad fetch. See console logs.</a></p>` + |
| `<p><a ${defaultMetaAttr}>Failed to fetch boo artifact: server error. See console logs.</a></p>` |
| ); |
| }); |
| |
| test('replaceTextArtifacts caches successful fetches', async () => { |
| const fetchStub = sandbox.stub(window, 'fetch'); |
| fetchStub.onCall(0).returns(Promise.reject(new Error('bad fetch'))); |
| fetchStub.onCall(1).returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => 'foobar test failed!', |
| } as Response) |
| ); |
| fetchStub.onCall(2).returns(Promise.reject(new Error('bad fetch'))); |
| |
| const artifacts = [ |
| { |
| artifactId: 'foobar', |
| name: 'foobar-name', |
| fetchUrl: 'https://foobar.com/', |
| sizeBytes: 10, |
| }, |
| ]; |
| |
| // Replace and fetch. |
| const html = '<p><text-artifact artifact-id="foobar" /></p>'; |
| assert.strictEqual( |
| await replaceTextArtifacts(html, artifacts, []), |
| `<p><a ${defaultMetaAttr}>Failed to fetch foobar artifact: bad fetch. See console logs.</a></p>` |
| ); |
| |
| assert.strictEqual( |
| await replaceTextArtifacts(html, artifacts, []), |
| '<p>foobar test failed!</p>' |
| ); |
| sinon.assert.calledTwice(fetch as any); |
| |
| // Check that fetches after the first successful fetch use cache. |
| assert.strictEqual( |
| await replaceTextArtifacts(html, artifacts, []), |
| '<p>foobar test failed!</p>' |
| ); |
| assert.strictEqual( |
| await replaceTextArtifacts(html, artifacts, []), |
| '<p>foobar test failed!</p>' |
| ); |
| sinon.assert.calledTwice(fetch as any); |
| }); |
| |
| test('replaceTextArtifacts links to large text artifacts', async () => { |
| const fetchStub = sandbox.stub(window, 'fetch'); |
| const artifactOne = 'a'.repeat(50001); |
| fetchStub.withArgs('https://a1.com/?n=50000').returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => artifactOne, |
| } as Response) |
| ); |
| const artifactTwo = 'a'.repeat(50002); |
| fetchStub.withArgs('https://a2.com/?n=50000').returns( |
| Promise.resolve({ |
| ok: true, |
| text: async () => artifactTwo, |
| } as Response) |
| ); |
| |
| const artifacts = [ |
| { |
| artifactId: 'a1', |
| name: 'a1-name', |
| fetchUrl: 'https://a1.com/', |
| sizeBytes: artifactOne.length, |
| }, |
| { |
| artifactId: 'a2', |
| name: 'a2-name', |
| fetchUrl: 'https://a2.com/', |
| sizeBytes: artifactTwo.length, |
| }, |
| ]; |
| |
| assert.strictEqual( |
| await replaceTextArtifacts( |
| '<p><text-artifact artifact-id="a1"/></p>', |
| artifacts, |
| [] |
| ), |
| `<p>${artifactOne}<a ${defaultMetaAttr}>\n\n...1 more byte...\n` + |
| 'More information in a1 artifact</a></p>' |
| ); |
| |
| assert.strictEqual( |
| await replaceTextArtifacts( |
| '<p><text-artifact artifact-id="a2"/></p>', |
| artifacts, |
| [] |
| ), |
| `<p>${artifactTwo}<a ${defaultMetaAttr}>\n\n...2 more bytes...\n` + |
| 'More information in a2 artifact</a></p>' |
| ); |
| }); |
| |
| test('parseMarkdown replaces headers', () => { |
| assert.strictEqual(parseMarkdown('#foo'), '#foo'); |
| assert.strictEqual(parseMarkdown('# foo'), '<h1>foo</h1>'); |
| assert.strictEqual(parseMarkdown('## foo'), '<h2>foo</h2>'); |
| assert.strictEqual(parseMarkdown('### foo'), '<h3>foo</h3>'); |
| assert.strictEqual(parseMarkdown('#### foo'), '<h4>foo</h4>'); |
| assert.strictEqual(parseMarkdown('##### foo'), '<h5>foo</h5>'); |
| assert.strictEqual(parseMarkdown('###### foo'), '<h6>foo</h6>'); |
| assert.strictEqual(parseMarkdown('####### foo'), '####### foo'); |
| assert.strictEqual( |
| parseMarkdown(`# foo |
| ### bar |
| ## baz`), |
| '<h1>foo</h1><h3>bar</h3><h2>baz</h2>' |
| ); |
| assert.strictEqual( |
| parseMarkdown('## foo __bar__'), |
| '<h2>foo <strong>bar</strong></h2>' |
| ); |
| assert.strictEqual( |
| parseMarkdown('__## foo bar__'), |
| '<strong>## foo bar</strong>' |
| ); |
| }); |
| |
| test('parseMarkdown adds strong', () => { |
| assert.strictEqual(parseMarkdown('_foo__'), '_foo__'); |
| assert.strictEqual(parseMarkdown('__foo__'), '<strong>foo</strong>'); |
| assert.strictEqual(parseMarkdown('**foo__'), '**foo__'); |
| assert.strictEqual(parseMarkdown('** foo**'), '** foo**'); |
| assert.strictEqual(parseMarkdown('**foo **'), '**foo **'); |
| assert.strictEqual(parseMarkdown('**foo**'), '<strong>foo</strong>'); |
| assert.strictEqual(parseMarkdown('**fo**'), '<strong>fo</strong>'); |
| assert.strictEqual(parseMarkdown('**f**'), '<strong>f</strong>'); |
| assert.strictEqual( |
| parseMarkdown('**space here**'), |
| '<strong>space here</strong>' |
| ); |
| assert.strictEqual(parseMarkdown('**newline\nhere**'), '**newline\nhere**'); |
| assert.strictEqual( |
| parseMarkdown('this __is__ **foo**'), |
| 'this <strong>is</strong> <strong>foo</strong>' |
| ); |
| assert.strictEqual(parseMarkdown('____'), '____'); |
| }); |
| |
| test('parseMarkdown replaces links', () => { |
| assert.strictEqual( |
| parseMarkdown('[foo](bar.com)'), |
| "<a href='bar.com'>foo</a>" |
| ); |
| assert.strictEqual( |
| parseMarkdown('[foo [baz]](bar.com)'), |
| "<a href='bar.com'>foo [baz]</a>" |
| ); |
| assert.strictEqual(parseMarkdown('[](bar.com)'), "<a href='bar.com'></a>"); |
| assert.strictEqual( |
| parseMarkdown('[a[c]](bar.com)[b[]](car.com)'), |
| "<a href='bar.com'>a[c]</a><a href='car.com'>b[]</a>" |
| ); |
| }); |
| |
| test('parseMarkdown creates unordered lists', () => { |
| const md = `# recipe |
| - **bacon** |
| - __lettuce__ |
| - tomatoes`; |
| assert.strictEqual( |
| parseMarkdown(md), |
| '<h1>recipe</h1><ul>' + |
| '<li><strong>bacon</strong></li>' + |
| '<li><strong>lettuce</strong></li>' + |
| '<li>tomatoes</li></ul>' |
| ); |
| }); |
| |
| test('parseMarkdown ignores markdown in code blocks', () => { |
| const md = `\`\`\`syntax highlighting here |
| don't format __this__ |
| # or even this! |
| \`\`\` |
| but this __should__ be okay |
| # and this __too__`; |
| assert.strictEqual( |
| parseMarkdown(md), |
| "<pre><code>don't format __this__\n# or even this!\n</code></pre>" + |
| 'but this <strong>should</strong> be okay' + |
| '\n<h1>and this <strong>too</strong></h1>' |
| ); |
| }); |
| |
| test('parseMarkdown ignores markdown in link URLs', () => { |
| const md = "__bold__. [Link](foo.com/__dont_bold__). **also bold**"; |
| assert.strictEqual( |
| parseMarkdown(md), |
| "<strong>bold</strong>. <a href='foo.com/__dont_bold__'>Link</a>. <strong>also bold</strong>" |
| ); |
| }); |
| }); |