update: disable ava test suites

This commit is contained in:
ThatGuySam 2024-12-27 14:48:54 -06:00
parent 44d7393039
commit fddfa9d5a4
7 changed files with 0 additions and 3 deletions

View file

@ -0,0 +1,94 @@
import test from 'ava'
import {
generateAPI
} from '~/helpers/api/client.js'
import {
isString
} from '~/helpers/check-types.js'
const listingsCases = [
// Spotify
[
'/api/app/spotify.json',
{
generateOptions: {},
method: DoesItAPI => DoesItAPI.app.spotify,
expected: {
name: 'Spotify',
}
}
],
// Electron
[
'/api/app/electron-framework.json',
{
generateOptions: {},
method: DoesItAPI => DoesItAPI.app('electron-framework'),
expected: { name: 'Electron Framework' }
}
],
// Express VPN
[
'/api/app/expressvpn.json',
{
generateOptions: {},
method: DoesItAPI => DoesItAPI.app.expressvpn,
expected: { name: 'ExpressVPN' }
}
],
// Solo App URL
[
'/api/app/solo.json',
{
generateOptions: {},
method: DoesItAPI => DoesItAPI.app('solo'),
expected: { name: 'SOLO' }
}
],
// Page 2 of App Kinds
[
'/api/kind/app/2.json',
{
generateOptions: {},
method: DoesItAPI => DoesItAPI('kind/app')(2),
expected: result => isString( result.nextPage )
}
]
]
test( 'API has valid responses', async t => {
// const { listingsDetails } = t.context
for ( const [ caseEndpoint, listingCase ] of listingsCases ) {
// const apiPath = listingsDetails[ caseEndpoint ].apiEndpointPath
const DoesItAPI = generateAPI( listingCase.generateOptions )
const apiMethod = listingCase.method( DoesItAPI )
// Assert that the apiMethod url is correct
t.is( (new URL(apiMethod.url)).pathname, caseEndpoint, `API endpoint '${ 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 }'` )
continue
}
t.like( result, listingCase.expected, `${ caseEndpoint } has a valid api endpoint` )
}
})

View file

@ -0,0 +1,97 @@
import {
isValidHttpUrl,
isValidImageUrl,
isNonEmptyString,
isPositiveNumberString
} from '~/helpers/check-types.js'
export const headPropertyTypes = {
'meta[charset]': {
charset: isNonEmptyString
},
// <meta name="viewport" content="width=device-width,initial-scale=1">
'meta[name="viewport"]': {
content: isNonEmptyString
},
// <meta property="og:image" content="https://doesitarm.com/images/og-image.png">
'meta[property="og:image"]': {
content: isValidImageUrl
},
// <meta property="og:image:width" content="1200">
'meta[property="og:image:width"]': {
content: isPositiveNumberString
},
// <meta property="og:image:height" content="627">
'meta[property="og:image:height"]': {
content: isPositiveNumberString
},
// <meta property="og:image:alt" content="Does It ARM Logo">
'meta[property="og:image:alt"]': {
content: isNonEmptyString
},
// <meta property="twitter:card" content="summary">
'meta[property="twitter:card"]': {
content: isNonEmptyString
},
// <meta property="twitter:title" content="Does It ARM">
'meta[property="twitter:title"]': {
content: isNonEmptyString
},
// <meta property="twitter:description" content="Find out the latest app support for Apple Silicon and the Apple M1 Pro and M1 Max Processors">
'meta[property="twitter:description"]': {
content: isNonEmptyString
},
// <meta property="twitter:url" content="https://doesitarm.com">
'meta[property="twitter:url"]': {
content: isValidHttpUrl
},
// <meta property="twitter:image" content="https://doesitarm.com/images/mark.png">
'meta[property="twitter:image"]': {
content: isValidImageUrl,
},
// <meta data-hid="description" name="description" content={ headDescription }>
'meta[name="description"]': {
content: isNonEmptyString
},
// <meta data-hid="twitter:title" property="twitter:title" content="Apple Silicon and Apple M1 Pro and M1 Max app and game compatibility list">
'meta[property="twitter:title"]': {
content: isNonEmptyString
},
// <link rel="icon" type="image/x-icon" href="/favicon.ico">
'link[rel="icon"]': {
href: isNonEmptyString
},
// <!-- Preconnect Assets -->
// <link rel="preconnect" href="https://www.googletagmanager.com">
// <link rel="preconnect" href="https://cdn.carbonads.com">
// <link rel="preconnect" href="https://srv.carbonads.net">
// <link rel="preconnect" href="https://cdn4.buysellads.net"></link>
'link[rel="preconnect"]': {
href: isValidHttpUrl
},
// <link rel="preload" as="image" href="large-image.webp" media="(min-width: 768px)" imagesrcset="large-image.webp, large-image-2x.webp 2x" type="image/webp" />
'link[rel="preload"]': {
as: isNonEmptyString,
href: isValidHttpUrl,
media: isNonEmptyString,
imagesrcset: isNonEmptyString,
type: isNonEmptyString,
count: false
},
}

View file

@ -0,0 +1,248 @@
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` )
}
})

216
test/_disabled/main.js Normal file
View file

@ -0,0 +1,216 @@
// import { promises as fs } from 'fs'
import fs from 'fs-extra'
import test from 'ava'
import parser from 'fast-xml-parser'
import axios from 'axios'
import { structuredDataTest } from 'structured-data-testing-tool'
import { Google } from 'structured-data-testing-tool/presets'
import {
sitemapIndexFileName
} from '~/helpers/constants.js'
import { logArraysDifference } from '~/helpers/array.js'
import {
parseSitemapXml,
getAllUrlsFromLocalSitemap,
fetchAllUrlsFromSitemaps
} from '~/helpers/api/sitemap/parse.js'
require('dotenv').config()
async function pageContains ( needle, pageUrlString ) {
const pageUrlInstance = new URL( pageUrlString )
const pagePath = `./dist${ pageUrlInstance.pathname }/index.html`
const pageHtml = await fs.readFile( pagePath , 'utf-8' )
return pageHtml.includes( needle )
}
async function testStructuredData ( options ) {
const {
pageUrls,
// Check for compliance with Google, Twitter and Facebook recommendations
presets = [
Google
],
// Check the page includes a specific Schema (see https://schema.org/docs/full.html for a list)
schemas
} = options
for ( const url of pageUrls ) {
const pagePath = `./dist${ url.pathname }/index.html`
const pageHtml = await fs.readFile( pagePath , 'utf-8' )
// https://github.com/glitchdigital/structured-data-testing-tool#api
await structuredDataTest( pageHtml , {
presets,
schemas
}).then(res => {
return res
}).catch(err => {
// console.log( 'err.res.failed', err.res.failed )
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
throw validationError
// return
}
throw new Error( 'Structured data testing error.', err )
})
// console.log('result', tvUrl.pathname, Object.keys( result ))
}
}
const sitemapFilesToTry = [
sitemapIndexFileName,
'sitemap.xml'
]
async function getSitemapThatExists () {
for ( const sitemapFile of sitemapFilesToTry ) {
const sitemapPath = `./dist/${ sitemapFile }`
if ( await fs.pathExists( sitemapPath ) ) {
return sitemapPath
}
}
}
test.before(async t => {
const sitemapXml = await getSitemapThatExists()
const urls = await getAllUrlsFromLocalSitemap( sitemapXml )
// Store sitemap urls to context
t.context.sitemapUrls = urls.map( tag => new URL( tag.loc ) )
})
test('Sitemap contains no double slashes in paths', (t) => {
// console.log('t.context.sitemapUrls', t.context.sitemapUrls)
const urlsWithDoubleSlashes = t.context.sitemapUrls.filter( url => url.pathname.includes('//') )
if ( urlsWithDoubleSlashes.length > 0) {
t.fail( `${ urlsWithDoubleSlashes.length } urls with doubles slashes found including ${ urlsWithDoubleSlashes[0] }` )
}
t.log( `${t.context.sitemapUrls.length} valid sitemap listings` )
t.pass()
})
test('Sitemap mostly matches production', async (t) => {
// console.log('t.context.sitemapUrls', t.context.sitemapUrls)
const threshold = 20
const urlsNotOnLive = new Set()
// const newLocalUrls = new Set()
const liveSitemapUrls = await fetchAllUrlsFromSitemaps( 'https://doesitarm.com' )
// Assert that any sitemap urls exist on live
t.assert( liveSitemapUrls.size > 0, 'No sitemap urls found on live.' )
// console.log( 'liveSitemapUrls', liveSitemapUrls )
for ( const localUrl of t.context.sitemapUrls ) {
const theoreticalLiveUrl = `https://doesitarm.com${ localUrl.pathname }`
if ( liveSitemapUrls.has( theoreticalLiveUrl ) ) {
liveSitemapUrls.delete( theoreticalLiveUrl )
continue
}
// localUrl is either: Missing or New
// urlsNotOnLive.add( theoreticalLiveUrl )
}
const message = `${ urlsNotOnLive.size } new or missing from live and ${ liveSitemapUrls.size } not found locally`
const totalDifferences = urlsNotOnLive.size + liveSitemapUrls.size
const liveSitemapUrlStrings = new Set( liveSitemapUrls.keys() )
if ( totalDifferences >= 0 ) {
t.log( 'Missing from live', urlsNotOnLive )
t.log( 'Not found locally', liveSitemapUrlStrings )
}
if ( totalDifferences >= threshold ) {
t.fail( message )
}
t.log( message )
t.pass()
})
// test('All TV pages have valid VideoObject structured data', async (t) => {
// const tvUrls = t.context.sitemapUrls.filter( url => url.pathname.startsWith('/tv/') )
// try {
// await testStructuredData({
// pageUrls: tvUrls,
// schemas: [ 'VideoObject' ]
// })
// } catch ( error ) {
// console.log('failed', error.failed)
// t.fail( error.message )
// }
// t.log( `${tvUrls.length} valid pages` )
// t.pass()
// })
// test('All App pages with bundle data have bundle data visuals', async (t) => {
// const appUrls = t.context.sitemapUrls.filter( url => url.pathname.startsWith('/app/') )
// const appsWithBundleIds = await fs.readJson('./static/app-list.json', 'utf-8').then( appList => {
// return appList.filter( app => {
// return app.bundleIds.length > 0
// })
// })
// t.log(`${appsWithBundleIds.length} apps with bundle IDs`)
// try {
// for ( const app of appsWithBundleIds ) {
// const hasAppBundlesSection = await pageContains( 'App Bundles', `${ process.env.URL }${app.endpoint}` )
// if ( !hasAppBundlesSection ) throw new Error(`Couldn't find App Bundles section on ${ app.endpoint }`)
// }
// } catch ( error ) {
// console.log('failed', error)
// t.fail( error.message )
// }
// t.log( `${appsWithBundleIds.length} valid app pages` )
// t.pass()
// })

View file

@ -0,0 +1,64 @@
import test from 'ava'
// import MarkdownIt from 'markdown-it'
// import { isValidHttpUrl } from '~/helpers/check-types.js'
import { StorkFilters } from '~/helpers/stork/browser.js'
test('Can Toggle off existing filter' , async t => {
const filters = new StorkFilters({
initialFilters: {
'test': 'yes'
}
})
filters.toggleFilter('test')
t.deepEqual(filters.has('test'), false)
filters.toggleFilter('test', 'yes')
filters.toggleFilter('status', 'native')
t.deepEqual(filters.has('test'), true, 'Has test filter')
t.deepEqual(filters.asQuery, 'test_yes status_native', 'Has correct filters for query')
filters.toggleFilter('status_native')
t.deepEqual(filters.asQuery, 'test_yes', 'Has only test filter')
})
test('Can handle query values with multiple underscores', async t => {
const filters = new StorkFilters({
initialFilters: {
'test': 'value_with_multiple_underscores'
}
})
t.log( 'filters.asQuery', filters.asQuery )
t.assert( filters.has( 'test_value_with_multiple_underscores' ) , 'Has correct filters for query' )
})
test( 'Can update existing filter', async t => {
const filters = new StorkFilters({
initialFilters: {
'test': 'works_no'
}
})
filters.toggleFilter('test_works_yes')
t.deepEqual( filters.asQuery, 'test_works_yes', 'Has updated filter')
})
test( 'Can set filters from string', async t => {
const filters = new StorkFilters()
filters.setFromString( 'test_works_yes' )
t.deepEqual( filters.asQuery, 'test_works_yes', 'Has updated filter')
})

View file

@ -0,0 +1,238 @@
import fs from 'fs-extra'
import test from 'ava'
// import MarkdownIt from 'markdown-it'
import { isValidHttpUrl } from '~/helpers/check-types.js'
import { buildReadmeAppList } from '~/helpers/build-app-list.js'
import {
matchesWholeWord,
fuzzyMatchesWholeWord,
eitherMatches
} from '~/helpers/matching.js'
import {
PaginatedList
} from '~/helpers/api/pagination.js'
require('dotenv').config()
const allowedTitleCharacters = new Set( 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890 -+:_.®/\()音乐体验版'.split('') )
// Detect Emojis(Extended Pictograph) in string
// https://stackoverflow.com/a/64007175/1397641
function hasEmoji ( string ) {
return /\p{Extended_Pictographic}/u.test( string )
}
test.before(async t => {
const readmeFileContent = await fs.readFile('./README.md', 'utf-8')
// Store sitemap urls to context
t.context.readmeFileContent = readmeFileContent
t.context.readmeAppList = buildReadmeAppList({
readmeContent: t.context.readmeFileContent,
scanListMap: new Map(),
commits: []
})
})
test('README Apps are formated correctly', (t) => {
// console.log('t.context.sitemapUrls', t.context.sitemapUrls)
const {
readmeAppList
} = t.context
// Store found apps so we can check for duplicates
const foundApps = new Set()
// Store found invalid apps so we can count and report them
const invalidApps = new Set()
for (const readmeApp of readmeAppList) {
const cleanedAppName = readmeApp.name//.toLowerCase()
// Check that app has not already been found
if (foundApps.has(cleanedAppName)) {
t.fail(`Duplicate app found: ${readmeApp.name}`)
invalidApps.add(cleanedAppName)
}
// Store this app so we can check for future duplicates
foundApps.add(cleanedAppName)
// Check that all related links urls are valid
for (const relatedLink of readmeApp.relatedLinks) {
if ( !isValidHttpUrl( relatedLink.href ) ) {
t.log('relatedLink.href', readmeApp.name, relatedLink.href)
t.fail(`README App ${readmeApp.name} does not have valid url`, readmeApp.url)
invalidApps.add(cleanedAppName)
}
}
// Check that status text is free of markdown
if ( readmeApp.text.includes('](') ) {
t.fail(`README App ${readmeApp.name} markdown in status text`)
invalidApps.add(cleanedAppName)
}
// Check that status has an emoji
if ( hasEmoji( readmeApp.text ) === false ) {
t.fail(`README App ${readmeApp.name} does not have emoji`)
invalidApps.add(cleanedAppName)
}
// Check for not allowed characters in app name
for ( const character of cleanedAppName ) {
if ( !allowedTitleCharacters.has( character ) ) {
// badCharacter = readmeApp.name[firstBadCharacterIndex]
// t.log( readmeApp )
t.fail(`README App Title ${readmeApp.name} has non-alphanumeric character ${character}(charCode ${character.charCodeAt(0)})`)
invalidApps.add(cleanedAppName)
}
}
}
t.log( readmeAppList.length - invalidApps.size, 'valid apps found' )
t.log( readmeAppList.length, 'apps found in README' )
t.pass()
})
function sortAppsAlphabetically ( a, b ) {
return a.name.localeCompare(b.name)
}
test('README Apps are in alphbetical order', (t) => {
const {
readmeAppList
} = t.context
const appsByCategory = new Map()
// Group apps by category
for ( const readmeApp of readmeAppList ) {
const category = readmeApp.category.slug
if ( !appsByCategory.has(category) ) {
appsByCategory.set(category, [])
}
appsByCategory.get( category ).push(readmeApp)
}
// Sort apps in groups alphabetically
for ( const [ category, apps ] of appsByCategory ) {
const unsortedApps = apps.slice()
// Sort apps in category in place
apps.sort(sortAppsAlphabetically)
// Check sorted sorted apps against unsorted apps
for ( const [ index, unsortedApp ] of unsortedApps.entries() ) {
const sortedApp = apps[index]
if ( sortedApp.slug !== unsortedApp.slug ) {
t.fail(`README App at index ${index} of ${category} is ${unsortedApp.name} but should be ${sortedApp.name}`)
}
}
}
t.pass()
})
const namesWithPlusses = [
'Xournal++',
'Notepad++'
]
test('Can match names with pluses', (t) => {
// Sort apps in groups alphabetically
for ( const nameWithPluses of namesWithPlusses ) {
const haystack = `FDKLS:KF ${nameWithPluses}NDFLSKFLSJDK`
t.assert( matchesWholeWord( nameWithPluses, haystack ) )
t.assert( fuzzyMatchesWholeWord( nameWithPluses, haystack ) )
t.assert( eitherMatches( nameWithPluses, haystack ) )
t.assert( eitherMatches( haystack, nameWithPluses ) )
}
t.pass()
})
test('Can paginate', async (t) => {
const cases = [
{
from: {
list: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ],
perPage: 2
},
expect: {
pageCount: 5,
pages: [
{
number: 1,
hasNextPage: true,
hasPreviousPage: false,
items: [1, 2],
json: '[1,2]'
},
{
number: 2,
hasNextPage: true,
hasPreviousPage: true,
items: [3, 4],
json: '[3,4]'
},
// Last page should have less than perPage items
{
number: 5,
hasNextPage: false,
hasPreviousPage: true,
items: [9],
json: '[9]'
}
]
}
}
]
for ( const { from, expect } of cases ) {
const paginatedList = new PaginatedList( from )
// Assert that page count is correct
t.is( paginatedList.pageCount, expect.pageCount, 'pageCount is incorrect' )
// Assert that the pages we're expecting are there
for ( const expectedPage of expect.pages ) {
// Get respective output page
const outputPage = paginatedList.pages[ expectedPage.number - 1 ]
t.deepEqual( outputPage, expectedPage, `Page ${ expectedPage.number } is an unexpected structure` )
}
}
} )

View file

@ -0,0 +1,145 @@
import {
describe,
it,
expect
} from 'vitest'
// Vitest Web Worker Compatibility - https://github.com/vitest-dev/vitest/tree/main/packages/web-worker
import '@vitest/web-worker'
import fs from 'fs-extra'
import path from 'node:path'
// https://github.com/mrmlnc/fast-glob
import glob from 'fast-glob'
import { LocalFileData } from 'get-file-object-from-local-path'
import { Zip } from 'zip-lib'
import { runScanWorker } from '~/helpers/scanner/client.mjs'
const appGlobOptions = {
onlyFiles: false,
deep: 1
}
const appsPath = 'test/_artifacts/apps'
const tempPath = 'test/_artifacts/temp'
// TODO: Unsupported Apps:
// Alt Tab 6.29.0 - Hangs
// AVTouchBar 3.0.6 - Times out
// Silicon - Fail with both MachoNode and MachoManiac
// arm_idafree76_mac 7.6 - Hangs
// Batteries 2.2.4 - Hangs
// BetterZip 5.1.1 - Hangs
// BlueJeans - May work but doesn't work with zip compression
// Coreform-Cubit - No info.plist, may not need to be supported
const plainAppBundles = glob
.sync( `${ appsPath }/**/*.app`, appGlobOptions )
.filter( bundlePath => {
return true
// return bundlePath.includes( 'AV' )
})
async function makeZipFromBundlePath ( bundlePath ) {
const archivePath = `${ tempPath }/${ bundlePath.split('/').pop() }.zip`
// Delete any existing archive
if ( await fs.exists( archivePath ) ) {
console.log( 'Deleting existing archive', archivePath )
await fs.unlink( archivePath )
}
// console.log( 'archivePath', archivePath )
// await compress( bundlePath, archivePath )
const zipLib = new Zip()
// Adds a folder from the file system, putting its contents at the root of archive
zipLib.addFolder( bundlePath )
// Generate zip file.
await zipLib.archive( archivePath )
// Create a File Object from the zip file
// https://developer.mozilla.org/en-US/docs/Web/API/File/File
const archiveFile = new LocalFileData( archivePath )
// console.log( 'archiveFile', archiveFile )
return archiveFile
}
describe.concurrent('Apps', async () => {
// Compress plain app bundles to zipped File Objects
for ( const bundlePath of plainAppBundles ) {
// Get the App's file name from bundlePath
const appName = path.basename( bundlePath )
// Generate a faux JavaScript File instance
const file = await makeZipFromBundlePath( bundlePath )
// Scan the app
const { scan } = await runScanWorker( file, ( details ) => {
console.log( 'New message from runScanWorker:', details )
} )
it( `Can read info.plist for ${ appName } bundle` , () => {
// console.log( 'infoPlist', scan.infoPlist )
expect( scan.hasInfoPlist ).toBe( true )
})
it( `Can read macho meta for entry ${ appName } bundle`, () => {
// console.log( 'machoMeta', scan.machoMeta )
expect( scan.hasMachoMeta ).toBe( true )
})
it( `Can provide scan info for ${ appName } bundle`, () => {
// console.log( 'machoMeta', scan.machoMeta )
expect( scan.hasInfo ).toBe( true )
// Expect the scan info to have a bundle name that matches the app name
expect( scan.info.filename ).toContain( appName )
// Expect app version is string
expect( typeof scan.info.appVersion ).toBe( 'string' )
// Expect that machoMeta is an object
expect( typeof scan.info.machoMeta ).toBe( 'object' )
// Expect that supportedArchitectures is not empty
expect( scan.supportedArchitectures.length ).toBeGreaterThan( 0 )
// Expect that processorType is a string
expect( typeof scan.supportedArchitectures[0].processorType ).toBe( 'string' )
const validCPUTypes = [
'ANY', 'VAX', 'MC680', 'X86', 'MIPS', 'MC98000', 'HPPA', 'ARM', 'ARM64', 'MC88000', 'SPARC', 'I860', 'POWERPC', 'POWERPC64'
]
// Expect that processorType is a valid CPU type
expect( validCPUTypes.includes( scan.supportedArchitectures[0].processorType ) ).toBe( true )
// Expect that first of machoMeta.architectures has processorSubType as string
// expect( typeof scan.supportedArchitectures[0].processorSubType ).toBeTruthy()
// Export info.infoPlist to be none empty object
expect( typeof scan.info.infoPlist ).toBe( 'object' )
expect( Object.keys( scan.info.infoPlist ).length ).toBeGreaterThan( 0 )
})
}
})