import fs from 'fs-extra' import has from 'just-has' import { test, expect, beforeAll } from 'vitest' 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 { getJson } from '~/helpers/http.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 } context.listings[caseEndpoint] = await getJson( `${process.env.PUBLIC_API_DOMAIN}${apiPath}` ) } // 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) } } })