diff --git a/lib/i18n.js b/lib/i18n.js index 409ad6c..a3b621a 100644 --- a/lib/i18n.js +++ b/lib/i18n.js @@ -98,6 +98,8 @@ const da = { 'Analysis date · {{date}}': 'Analysedato · {{date}}', 'Analysis date': 'Analysedato', 'Awaiting analysis': 'Afventer analyse', + 'Analysis pending': 'Analyse afventer', + 'This page refreshes automatically while analysis is pending.': 'Denne side opdateres automatisk, mens analysen afventes.', 'See on the App Store ↗': 'Se i App Store ↗', 'Report metadata': 'Rapportmetadata', 'Analysed version': 'Analyseret version', diff --git a/public/css/styles.css b/public/css/styles.css index 196012c..9f19316 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -274,6 +274,7 @@ progress.bar-track::-moz-progress-bar { background: var(--brand); } .notice p:last-child { margin-bottom: 0; } .notice--quiet { background: var(--warm); color: #544646; } .notice--warning { background: #f4ece1; border-left-color: #95652c; color: #4f3c27; } +.pending-analysis__progress { accent-color: var(--brand); display: block; height: .65rem; margin: 1rem 0; width: 100%; } .error-list { margin: 0; padding-left: 1.2rem; } .directory-controls, .tracker-controls { display: grid; gap: .5rem .75rem; grid-template-columns: auto minmax(160px, 1fr) auto; margin: 1.5rem 0 1rem; } diff --git a/public/js/analysis-pending.js b/public/js/analysis-pending.js new file mode 100644 index 0000000..00f0c98 --- /dev/null +++ b/public/js/analysis-pending.js @@ -0,0 +1,7 @@ +(function () { + 'use strict'; + + window.setTimeout(function () { + window.location.reload(); + }, 30000); +}()); diff --git a/test/websiteViews.test.js b/test/websiteViews.test.js index 853c176..ef1ce7d 100644 --- a/test/websiteViews.test.js +++ b/test/websiteViews.test.js @@ -1,8 +1,10 @@ 'use strict'; const assert = require('node:assert/strict'); +const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); +const vm = require('node:vm'); const pug = require('pug'); const { createTranslator, formatDate, formatDateTime, languageUrl, localUrl } = require('../lib/i18n'); @@ -113,6 +115,7 @@ test('production templates preserve the principal page states and data', () => { assert.match(success, /Camera/); assert.match(success, /href="\/tracker\/acme-analytics"/); assert.match(success, /href="\/company\/acme-corp"/); + assert.doesNotMatch(render('form.pug', { ...reportBase, analyserOnline: true }), /analysis-pending\.js/); const noTrackers = render('form.pug', { ...reportBase, @@ -142,19 +145,47 @@ test('production templates preserve the principal page states and data', () => { assert.match(failed, /Analysis failed/); assert.doesNotMatch(failed, /Tracking software/); assert.doesNotMatch(failed, /Acme Analytics/); + assert.doesNotMatch(render('form.pug', { ...reportBase, app: { ...reportBase.app, analysis: { success: false } }, analyserOnline: true }), /analysis-pending\.js/); - const pending = render('form.pug', { + const pendingData = { app: { appid: 'com.example.pending', details: { title: 'Pending App', version: '2.0.0' }, reportMetadata: { title: 'Pending App', queueVersion: '2.0.0', currentVersion: '2.0.0', currentVersionFromStorefront: false }, queueCount: 0 - }, - analyserOnline: false - }); + } + }; + const pending = render('form.pug', { ...pendingData, analyserOnline: false }); assert.match(pending, /Awaiting analysis/); assert.match(pending, /next in the analysis queue/); assert.match(pending, /currently offline/); assert.match(pending, /Queue-time version/); + assert.doesNotMatch(pending, /analysis-pending\.js|pending-analysis__progress/); + + const pendingOnline = render('form.pug', { ...pendingData, analyserOnline: true }); + assert.match(pendingOnline, /]*pending-analysis__progress[^>]*>/); + assert.match(pendingOnline, /refreshes automatically/); + assert.match(pendingOnline, /src="\/js\/analysis-pending\.js"/); + assert.doesNotMatch(pendingOnline, /currently offline/); + + const pendingDanish = render('form.pug', { ...pendingData, analyserOnline: true, locale: 'da', t: createTranslator('da') }); + assert.match(pendingDanish, /Denne side opdateres automatisk/); +}); + +test('pending analysis script schedules a fresh report request', () => { + const script = fs.readFileSync(path.join(__dirname, '..', 'public/js/analysis-pending.js'), 'utf8'); + let refresh; + let interval; + let reloads = 0; + vm.runInNewContext(script, { + window: { + setTimeout(callback, milliseconds) { refresh = callback; interval = milliseconds; }, + location: { reload() { reloads += 1; } } + } + }); + assert.equal(interval, 30000); + assert.equal(reloads, 0); + refresh(); + assert.equal(reloads, 1); }); test('search, directory, lookup, statistics, about, request and error states render safely', () => { diff --git a/views/form.pug b/views/form.pug index 9568518..3981378 100644 --- a/views/form.pug +++ b/views/form.pug @@ -228,6 +228,9 @@ block app p= translate(report.queueCount === 1 ? 'This app is queued for analysis. {{count}} other app will be analysed first.' : 'This app is queued for analysis. {{count}} other apps will be analysed first.', { count: report.queueCount || 0 }) if typeof analyserOnline !== 'undefined' && analyserOnline === false p= translate('Analyser service is currently offline. Analysis will resume when the service is available.') + else if typeof analyserOnline !== 'undefined' && analyserOnline === true + progress.pending-analysis__progress(aria-label=translate('Analysis pending')) + p= translate('This page refreshes automatically while analysis is pending.') else .report-summary(aria-label=translate('Report summary')) .summary-item @@ -372,3 +375,7 @@ block app | . p.stale-note= translate('Store details are retained from the last recorded metadata check and may be stale. They do not verify the current app version or current behaviour.') + +block scripts + if typeof app !== 'undefined' && app && !app.analysis && typeof analyserOnline !== 'undefined' && analyserOnline === true + script(src=assets + '/js/analysis-pending.js' defer)