From de1843c2bfa6d8fea95c70cb0fba0beda5450709 Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Fri, 27 Dec 2024 15:23:50 -0600 Subject: [PATCH 1/7] update: add .test extension --- test/listings/{index.js => index.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/listings/{index.js => index.test.js} (100%) diff --git a/test/listings/index.js b/test/listings/index.test.js similarity index 100% rename from test/listings/index.js rename to test/listings/index.test.js From 559e47c8fae0464bd0ac7c45c75ee6ffa28b25b5 Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Fri, 27 Dec 2024 15:25:57 -0600 Subject: [PATCH 2/7] update: enable helpers again --- test/{_disabled => }/helpers/head.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{_disabled => }/helpers/head.js (100%) diff --git a/test/_disabled/helpers/head.js b/test/helpers/head.js similarity index 100% rename from test/_disabled/helpers/head.js rename to test/helpers/head.js From a232a1b49d23e615286be56496cff57e6be61fdb Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Sat, 11 Jan 2025 19:19:45 -0600 Subject: [PATCH 3/7] update: initial vitest refactor for listings --- test/listings/index.test.js | 248 ------------------------------------ test/listings/index.test.ts | 229 +++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 248 deletions(-) delete mode 100644 test/listings/index.test.js create mode 100644 test/listings/index.test.ts diff --git a/test/listings/index.test.js b/test/listings/index.test.js deleted file mode 100644 index 9b7cdb3..0000000 --- a/test/listings/index.test.js +++ /dev/null @@ -1,248 +0,0 @@ -import fs from 'fs-extra' -import has from 'just-has' -import test from 'ava' -import axios from 'axios' -import { JSDOM } from 'jsdom' -import { structuredDataTestHtml } from 'structured-data-testing-tool' -import { Google } from 'structured-data-testing-tool/presets' - - -import { - makeApiPathFromEndpoint, - getVideoImages, - ListingDetails -} from '~/helpers/listing-page.js' -import { headPropertyTypes } from '~/test/helpers/head.js' -import { PageHead } from '~/helpers/config-node.js' - - - -const listingsCases = { - - // Spotify - '/app/spotify': { - endpoint: '/app/spotify', - apiEndpointPath: '/api/app/spotify.json', - expectInitialVideo: true, - shouldHaveVideoStucturedData: false, - }, - - // Electron - '/app/electron-framework': { - endpoint: '/app/electron-framework', - apiEndpointPath: '/api/app/electron-framework.json', - expectInitialVideo: false, - shouldHaveVideoStucturedData: false, - }, - - // Express VPN Benchmarks - '/app/expressvpn/benchmarks/': { - endpoint: '/app/expressvpn/benchmarks/', - apiEndpointPath: '/api/app/expressvpn.json', - expectInitialVideo: true, - shouldHaveVideoStucturedData: true - }, - - // Express VPN Benchmarks - '/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i/': { - endpoint: '/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i/', - apiEndpointPath: '/api/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i.json', - expectInitialVideo: true, - shouldHaveVideoStucturedData: true - } -} - -const listingCaseEntries = Object.entries( listingsCases ) - -test.before(async t => { - - t.context.listings = {} - - for ( const [ caseEndpoint, listingCase ] of listingCaseEntries ) { - const { endpoint } = listingCase - - const apiPath = makeApiPathFromEndpoint( caseEndpoint ) - const localPath = `./static${ apiPath }` - - // Check if the endpoint exists locally - // so we don't have to wait for the API - if ( await fs.pathExists( localPath ) ) { - console.log('Using local endpoint data for', endpoint) - t.context.listings[ caseEndpoint ] = await fs.readJson( localPath ) - continue - } - - const { data } = await axios.get(`${ process.env.PUBLIC_API_DOMAIN }${ apiPath }`) - - t.context.listings[ caseEndpoint ] = data - } - - t.context.listingsDetails = {} - - for ( const [ caseEndpoint ] of listingCaseEntries ) { - t.context.listingsDetails[ caseEndpoint ] = new ListingDetails( t.context.listings[ caseEndpoint ] ) - } -}) - - -function parseHTML ( htmlString ) { - const dom = new JSDOM( htmlString ) - - return { - dom, - window: dom.window, - document: dom.window.document - } -} - -test( 'Listings have valid api endpoints', async t => { - const { listingsDetails } = t.context - - for ( const [ caseEndpoint, listingCase ] of listingCaseEntries ) { - - const apiPath = listingsDetails[ caseEndpoint ].apiEndpointPath - - t.assert( listingCase.apiEndpointPath === apiPath, `${ caseEndpoint } has a valid api endpoint` ) - } -}) - -test( 'Listings with videos have preload data for initialVideo', async t => { - const { listingsDetails } = t.context - - for ( const [ caseEndpoint, listingCase ] of listingCaseEntries ) { - - const listingDetails = listingsDetails[ caseEndpoint ] - - t.assert( listingDetails.hasInitialVideo === listingCase.expectInitialVideo, `${ caseEndpoint } has initial video` ) - - // Stop here if we don't have an initial video - if ( !listingDetails.hasInitialVideo ) continue - - // t.log('listingDetails.initialVideo', listingDetails.initialVideo) - - // Get headProperties for image preloading - const preloadHeadChecks = headPropertyTypes[ 'link[rel="preload"]' ] - - const images = getVideoImages( listingDetails.initialVideo ) - - // Check if the head object properties are correct - for ( const preload of images.preloads ) { - for ( const [ propertyName, checkMethod ] of Object.entries( preloadHeadChecks ) ) { - // Skip count property - if ( propertyName === 'count' ) continue - - const value = preload[ propertyName ] - - t.assert( checkMethod( value ), `${ propertyName } failed. Value is '${ value }' for '${ images.imgSrc }'` ) - } - } - } -}) - - -test('Listings have valid headings', async t => { - const { listingsDetails } = t.context - - for ( const [ caseEndpoint, listingCase ] of listingCaseEntries ) { - - - // Build listing details - const listingDetails = listingsDetails[ caseEndpoint ] - const listingPageHead = new PageHead( listingDetails.headOptions ) - - // console.log( 'pageMeta', listingPageHead.metaMarkup ) - - // Parse into dom - // so we can get data via selectors - const { document } = parseHTML( listingPageHead.metaAndLinkMarkup ) - - for ( const [ selector, checks ] of Object.entries( headPropertyTypes ) ) { - const elements = document.querySelectorAll( selector ) - - let count = 1 - - if ( has( checks, 'count' ) ) { - count = checks.count - // delete checks.count - } - - if ( count !== false ) { - // Fail if there's more or less than one element - t.is( elements.length, count, `${ selector } count is ${ elements.length } but should be ${ count }` ) - } - - for( const element of elements ) { - for ( const [ check, checkMethod ] of Object.entries( checks ) ) { - // console.log( `Ckecking ${ selector } ${ check }` ) - - const value = element.getAttribute( check ) - - t.assert( checkMethod( value ), `${ check } on ${ selector } failed. Value is '${ value }'` ) - } - } - - } - - } -}) - - -test( 'Listings with videos have structured data', async t => { - const { listingsDetails } = t.context - - for ( const [ caseEndpoint, listingCase ] of listingCaseEntries ) { - - const listingDetails = listingsDetails[ caseEndpoint ] - const listingPageHead = new PageHead( { - ...listingDetails.headOptions, - pathname: caseEndpoint - }) - - // Stop here if we're not expecting Video Structured Data - if ( !listingCase.shouldHaveVideoStucturedData ) { - - // Check that the non-video listing doesn't have video structured data - t.assert( !listingPageHead.structuredDataMarkup.includes('VideoObject'), `${ caseEndpoint } has video structured data` ) - - continue - } - - - - // t.log('listingDetails.initialVideo', listingDetails.initialVideo) - // t.log( 'caseEndpoint', caseEndpoint ) - - // Assert that the structured data is not empty - t.assert( listingPageHead.structuredDataMarkup.trim() !== '', `${ caseEndpoint } has structured data` ) - - // https://github.com/glitchdigital/structured-data-testing-tool#api - const testResult = await structuredDataTestHtml( listingPageHead.allHeadMarkup , { - presets: [ Google ], - schemas: [ 'VideoObject' ] - }).then(res => { - return res - }).catch(err => { - // console.warn( 'Structured Data error', err.error ) - - if (err.type === 'VALIDATION_FAILED') { - - // t.fail( 'Some structured data tests failed.' ) - const validationError = new Error( 'Some structured data tests failed.' ) - - validationError.failed = err.res.failed - validationError.errors = Array.from( err.res.failed ).map( fail => fail.error ) - - throw validationError - - // return - } - - throw new Error( 'Structured data testing error.', err.error ) - }) - - // t.log( 'testResult', testResult ) - - // Assert that no Structured Data tests failed - t.assert( testResult.failed.length === 0, `${ caseEndpoint } has valid structured data` ) - } -}) diff --git a/test/listings/index.test.ts b/test/listings/index.test.ts new file mode 100644 index 0000000..e474c68 --- /dev/null +++ b/test/listings/index.test.ts @@ -0,0 +1,229 @@ +import fs from 'fs-extra' +import has from 'just-has' +import { test, expect, beforeAll } from 'vitest' +import axios from 'axios' +import { JSDOM } from 'jsdom' +import { structuredDataTestHtml } from 'structured-data-testing-tool' +import { Google } from 'structured-data-testing-tool/presets' + +import { + makeApiPathFromEndpoint, + getVideoImages, + ListingDetails +} from '~/helpers/listing-page.js' +import { headPropertyTypes } from '~/test/helpers/head.js' +import { PageHead } from '~/helpers/config-node.js' + +const listingsCases = { + // Spotify + '/app/spotify': { + endpoint: '/app/spotify', + apiEndpointPath: '/api/app/spotify.json', + expectInitialVideo: true, + shouldHaveVideoStructuredData: false, + }, + + // Electron + '/app/electron-framework': { + endpoint: '/app/electron-framework', + apiEndpointPath: '/api/app/electron-framework.json', + expectInitialVideo: false, + shouldHaveVideoStructuredData: false, + }, + + // Express VPN Benchmarks + '/app/expressvpn/benchmarks/': { + endpoint: '/app/expressvpn/benchmarks/', + apiEndpointPath: '/api/app/expressvpn.json', + expectInitialVideo: true, + shouldHaveVideoStructuredData: true + }, + + // Express VPN Benchmarks + '/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i/': { + endpoint: '/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i/', + apiEndpointPath: '/api/tv/install-instagram-app-on-m1-macbook-air-apple-silicon-tutorial-i-vfbmworal6i.json', + expectInitialVideo: true, + shouldHaveVideoStructuredData: true + } +} + +const listingCaseEntries = Object.entries(listingsCases) + +interface TestContext { + listings: Record; + listingsDetails: Record; +} + +let context: TestContext = { + listings: {}, + listingsDetails: {} +} + +beforeAll(async () => { + for (const [caseEndpoint, listingCase] of listingCaseEntries) { + const { endpoint } = listingCase + const apiPath = makeApiPathFromEndpoint(caseEndpoint) + const localPath = `./static${apiPath}` + + // Check if endpoint exists locally to avoid API calls + if (await fs.pathExists(localPath)) { + console.log('Using local endpoint data for', endpoint) + context.listings[caseEndpoint] = await fs.readJson(localPath) + continue + } + + const { data } = await axios.get(`${process.env.PUBLIC_API_DOMAIN}${apiPath}`) + context.listings[caseEndpoint] = data + } + + // Initialize listing details + for (const [caseEndpoint] of listingCaseEntries) { + context.listingsDetails[caseEndpoint] = new ListingDetails(context.listings[caseEndpoint]) + } +}) + +function parseHTML(htmlString: string) { + const dom = new JSDOM(htmlString) + return { + dom, + window: dom.window, + document: dom.window.document + } +} + +test('Listings have valid api endpoints', async () => { + const { listingsDetails } = context + + for (const [caseEndpoint, listingCase] of listingCaseEntries) { + const apiPath = listingsDetails[caseEndpoint].apiEndpointPath + expect(listingCase.apiEndpointPath).toBe(apiPath) + } +}) + +test.todo('Listings with videos have preload data for initialVideo', async () => { + const { listingsDetails } = context + + for (const [caseEndpoint, listingCase] of listingCaseEntries) { + const listingDetails = listingsDetails[caseEndpoint] + + expect( + listingDetails.hasInitialVideo === listingCase.expectInitialVideo, + `${caseEndpoint} has initial video` + ).toBeTruthy() + + // Stop here if we don't have an initial video + if (!listingDetails.hasInitialVideo) continue + + // Get headProperties for image preloading + const preloadHeadChecks = headPropertyTypes['link[rel="preload"]'] + const images = getVideoImages(listingDetails.initialVideo) + + // Check if the head object properties are correct + for (const preload of images.preloads) { + for (const [propertyName, checkMethod] of Object.entries(preloadHeadChecks)) { + // Skip count property + if (propertyName === 'count') continue + + const value = preload[propertyName] + expect( + checkMethod, + `${propertyName} failed. Value is '${value}' for '${images.imgSrc}'` + ).toBeTruthy() + } + } + } +}) + +test.todo('Listings have valid headings', async () => { + const { listingsDetails } = context + + for (const [caseEndpoint] of listingCaseEntries) { + // Build listing details + const listingDetails = listingsDetails[caseEndpoint] + const listingPageHead = new PageHead(listingDetails.headOptions) + + // Parse into dom so we can get data via selectors + const { document } = parseHTML(listingPageHead.metaAndLinkMarkup) + + for (const [selector, checks] of Object.entries(headPropertyTypes)) { + const elements = document.querySelectorAll(selector) + + let count = 1 + if (has(checks, 'count')) { + count = checks.count + } + + if ( count !== 0 ) { + // Fail if there's more or less than one element + expect( + elements.length, + `${selector} count is ${elements.length} but should be ${count}` + ).toBe(count) + } + + for (const element of elements) { + for (const [check, checkMethod] of Object.entries(checks)) { + const value = element.getAttribute(check) + expect( + checkMethod, + `${check} on ${selector} failed. Value is '${value}'` + ).toBeTruthy() + } + } + } + } +}) + +test.todo('Listings with videos have structured data', async () => { + const { listingsDetails } = context + + for (const [caseEndpoint, listingCase] of listingCaseEntries) { + const listingDetails = listingsDetails[caseEndpoint] + const listingPageHead = new PageHead({ + ...listingDetails.headOptions, + pathname: caseEndpoint + }) + + // Stop here if we're not expecting Video Structured Data + if (!listingCase.shouldHaveVideoStructuredData) { + // Check that the non-video listing doesn't have video structured data + expect( + !listingPageHead.structuredDataMarkup.includes('VideoObject'), + `${caseEndpoint} has video structured data` + ).toBeTruthy() + continue + } + + // Assert that the structured data is not empty + expect( + listingPageHead.structuredDataMarkup.trim() !== '', + `${caseEndpoint} has structured data` + ).toBeTruthy() + + try { + const testResult = await structuredDataTestHtml( + listingPageHead.allHeadMarkup, + { + presets: [Google], + schemas: ['VideoObject'] + } + ) + + // Assert that no Structured Data tests failed + expect( + testResult.failed.length, + `${caseEndpoint} has valid structured data` + ).toBe(0) + } catch (err) { + if (err.type === 'VALIDATION_FAILED') { + console.error({ + failed: err.res.failed, + // errors: Array.from(err.res.failed).map(fail => fail?.error!) + }) + throw new Error('Some structured data tests failed. ') + } + throw new Error('Structured data testing error.', err.error) + } + } +}) From 7c8adea94145ae5fda24ab559ff76d68fb6a62b1 Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Sat, 11 Jan 2025 19:20:00 -0600 Subject: [PATCH 4/7] update: test listings with vitest --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83eda74..b8bfcfc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "scripts": { "test-prebuild": "pnpm run with-env vitest ./test/prebuild", "test-api-client": "ava ./test/api/client.js --verbose", - "test-listings": "ava ./test/listings/**/*.js --verbose", + "test-listings": "pnpm run with-env vitest ./test/listings", "test-postbuild-api": "pnpm test-listings", "test-vitest": "vitest", "test": "ava --timeout=1m --verbose", From 0afc64c5e94f43bf27c2074c041919cab53e634e Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Sat, 11 Jan 2025 19:27:02 -0600 Subject: [PATCH 5/7] update: enable api client test again --- test/{_disabled/api/client.js => api/client.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{_disabled/api/client.js => api/client.test.js} (100%) diff --git a/test/_disabled/api/client.js b/test/api/client.test.js similarity index 100% rename from test/_disabled/api/client.js rename to test/api/client.test.js From f76a41a2e1040df2089d5fe1b6a079850423dd00 Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Sat, 11 Jan 2025 19:27:17 -0600 Subject: [PATCH 6/7] update: use vitest for api tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8bfcfc..44a6143 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "scripts": { "test-prebuild": "pnpm run with-env vitest ./test/prebuild", - "test-api-client": "ava ./test/api/client.js --verbose", + "test-api-client": "pnpm run with-env vitest ./test/api", "test-listings": "pnpm run with-env vitest ./test/listings", "test-postbuild-api": "pnpm test-listings", "test-vitest": "vitest", From c16868da133a3d5f21bd4cae74bf9820a43045db Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Sat, 11 Jan 2025 19:40:07 -0600 Subject: [PATCH 7/7] update: refactor assertions to work again --- test/api/client.test.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/test/api/client.test.js b/test/api/client.test.js index 246678a..5aece2b 100644 --- a/test/api/client.test.js +++ b/test/api/client.test.js @@ -1,4 +1,4 @@ -import test from 'ava' +import { describe, expect, test } from 'vitest' import { generateAPI @@ -76,19 +76,34 @@ test( 'API has valid responses', async t => { const apiMethod = listingCase.method( DoesItAPI ) // Assert that the apiMethod url is correct - t.is( (new URL(apiMethod.url)).pathname, caseEndpoint, `API endpoint '${ caseEndpoint }'` ) + // t.is( (new URL(apiMethod.url)).pathname, caseEndpoint, `API endpoint '${ caseEndpoint }'` ) + expect( + (new URL(apiMethod.url)).pathname, + `API endpoint '${ caseEndpoint }'` + ).toBe(caseEndpoint) // Run get request to fetch our data const result = await apiMethod.get() // If expected is a function then call it - // Otherwise, compare the result to the expected if ( typeof listingCase.expected === 'function' ) { - t.assert( listingCase.expected( result ), `API case method check for '${ caseEndpoint }'` ) + // t.assert( listingCase.expected( result ), `API case method check for '${ caseEndpoint }'` ) + expect( + listingCase.expected(result), + `API case method check for '${ caseEndpoint }'` + ).toBeTruthy() continue } - t.like( result, listingCase.expected, `${ caseEndpoint } has a valid api endpoint` ) + + + // t.like( result, listingCase.expected, `${ caseEndpoint } has a valid api endpoint` ) + expect( + result, + `${ caseEndpoint } has a valid api endpoint` + ).toEqual( + expect.objectContaining(listingCase.expected) + ) } })