@anzusystems/common-admin 2.0.0-rc.7 → 2.0.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "./package.json": "./package.json"
42
42
  },
43
- "version": "2.0.0-rc.7",
43
+ "version": "2.0.0-rc.8",
44
44
  "license": "Apache-2.0",
45
45
  "repository": {
46
46
  "type": "git",
@@ -80,7 +80,7 @@
80
80
  "release:cleanup": "npx -y --allow-git=all github:anzusystems/release-tools#main cleanup"
81
81
  },
82
82
  "devDependencies": {
83
- "@anzusystems/common-admin": "2.0.0-rc.7",
83
+ "@anzusystems/common-admin": "2.0.0-rc.8",
84
84
  "@intlify/unplugin-vue-i18n": "^11.2.8",
85
85
  "@mdi/font": "^7.4.47",
86
86
  "@sentry/vite-plugin": "11.10.0",
@@ -38,6 +38,7 @@ export declare const visitRoute: VisitRoute
38
38
 
39
39
  /**
40
40
  * The first record a listing shows: from a link to it, else by clicking the first row and reading where the
41
- * app went. Kept in a file in `outputDir`, so a record's detail and edit share one visit of the listing.
41
+ * app went. An answer the listing gives every time (a record, no record, not a page) is kept in a file in
42
+ * `outputDir`, so a record's detail and edit share one visit of the listing; one a failed or empty load gave is not.
42
43
  */
43
44
  export declare const firstRecordOf: FirstRecordOf
@@ -348,8 +348,9 @@ const unresolvedComponents = (page) =>
348
348
 
349
349
  /**
350
350
  * Opens the route on a fresh document. `about:blank` first, so that what the previous page still logs or
351
- * requests while it unloads is not blamed on this one. An app that comes up blank is reloaded once: a cold
352
- * boot can hit a transient failure that the next one does not.
351
+ * requests while it unloads is not blamed on this one. An app that comes up blank, without a `main` or with
352
+ * only the start-up loader's empty one, is reloaded once: a cold boot can hit a transient failure that the next
353
+ * one does not. A page that stays empty after that is reported as one.
353
354
  *
354
355
  * @param {Page} page
355
356
  * @param {PageWatcher} watcher
@@ -360,14 +361,13 @@ const openRoute = async (page, watcher, target, timings) => {
360
361
  await page.goto('about:blank')
361
362
  watcher.reset(timings)
362
363
  await page.goto(target)
363
- try {
364
- return await settle(page, watcher, timings)
365
- } catch (error) {
366
- if ((await page.locator('main').count()) > 0) throw error
367
- watcher.reset(timings)
368
- await page.reload()
369
- return settle(page, watcher, timings)
370
- }
364
+ const first = await settle(page, watcher, timings).catch((/** @type {Error} */ error) => error)
365
+ if (typeof first === 'boolean' && (await pageState(page))?.hasText) return first
366
+ // A loader that never ends on a page that rendered is the page's failure, not the boot's.
367
+ if (first instanceof Error && (await page.locator('main').count()) > 0) throw first
368
+ watcher.reset(timings)
369
+ await page.reload()
370
+ return settle(page, watcher, timings)
371
371
  }
372
372
 
373
373
  /** @type {import('./types.mjs').VisitRoute} */
@@ -405,12 +405,19 @@ export async function firstRecordOf(
405
405
  const file = recordFile(outputDir, listing, pattern)
406
406
  if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8'))
407
407
 
408
- const lookup = await (async () => {
409
- const listingTimings = withDefaults(timings)
410
- watcher.reset(listingTimings)
411
- await page.goto(listing)
412
- await settle(page, watcher, listingTimings).catch(() => false)
413
- if ((await page.locator(NOT_FOUND_SELECTOR).count()) > 0) return { missing: `${listing} is not a page` }
408
+ // `keep`: an answer the listing gives every time. One that a failed load or a page still loading gave is not kept,
409
+ // so the routes after it look again instead of all failing on what went wrong once.
410
+ /** @type {{ lookup: RecordLookup; keep: boolean }} */
411
+ const { lookup, keep } = await (async () => {
412
+ const opened = await openRoute(page, watcher, listing, withDefaults(timings)).then(
413
+ () => undefined,
414
+ (/** @type {Error} */ error) => error
415
+ )
416
+ if (opened) return { lookup: { missing: `${listing} did not load: ${opened.message}` }, keep: false }
417
+ if ((await page.locator(NOT_FOUND_SELECTOR).count()) > 0) {
418
+ return { lookup: { missing: `${listing} is not a page` }, keep: true }
419
+ }
420
+ if (!(await pageState(page))?.hasText) return { lookup: { missing: `${listing} came up empty` }, keep: false }
414
421
 
415
422
  const fromLink = await page.evaluate(
416
423
  ({ prefix, source }) => {
@@ -421,15 +428,15 @@ export async function firstRecordOf(
421
428
  },
422
429
  { prefix: listing, source: pattern.source }
423
430
  )
424
- if (fromLink !== undefined) return { id: fromLink }
431
+ if (fromLink !== undefined) return { lookup: { id: fromLink }, keep: true }
425
432
 
426
433
  const firstRow = page.locator(rowSelector).first()
427
434
  if ((await firstRow.count()) === 0) {
428
435
  // Empty only when the table says so: a row selector that matches nothing must not pass for no data.
429
436
  if (await page.locator(NO_DATA_SELECTOR).first().isVisible()) {
430
- return { missing: `${listing} shows no record`, empty: /** @type {const} */ (true) }
437
+ return { lookup: { missing: `${listing} shows no record`, empty: /** @type {const} */ (true) }, keep: true }
431
438
  }
432
- return { missing: `nothing in ${listing} matches the row selector` }
439
+ return { lookup: { missing: `nothing in ${listing} matches the row selector` }, keep: false }
433
440
  }
434
441
  // The corner of the first cell, inside its padding: a cell can hold a link to another record
435
442
  // (a site group, a super author), and a click on it would open that one instead.
@@ -439,15 +446,19 @@ export async function firstRecordOf(
439
446
  .click({ position: { x: 3, y: 3 } })
440
447
  await page.waitForURL((url) => url.pathname.startsWith(`${listing}/`), { timeout: 10_000 }).catch(() => {})
441
448
  const candidate = new URL(page.url()).pathname.slice(listing.length + 1).split('/')[0] ?? ''
442
- if (!pattern.test(candidate)) return { missing: `clicking the first row of ${listing} opened ${page.url()}` }
449
+ if (!pattern.test(candidate)) {
450
+ return { lookup: { missing: `clicking the first row of ${listing} opened ${page.url()}` }, keep: false }
451
+ }
443
452
 
444
- return { id: candidate }
453
+ return { lookup: { id: candidate }, keep: true }
445
454
  })()
446
- // Written aside and renamed: another worker reading meanwhile sees the whole file or none.
447
- fs.mkdirSync(path.dirname(file), { recursive: true })
448
- const written = `${file}.${process.pid}.tmp`
449
- fs.writeFileSync(written, JSON.stringify(lookup))
450
- fs.renameSync(written, file)
455
+ if (keep) {
456
+ // Written aside and renamed: another worker reading meanwhile sees the whole file or none.
457
+ fs.mkdirSync(path.dirname(file), { recursive: true })
458
+ const written = `${file}.${process.pid}.tmp`
459
+ fs.writeFileSync(written, JSON.stringify(lookup))
460
+ fs.renameSync(written, file)
461
+ }
451
462
 
452
463
  return lookup
453
464
  }