|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// `sys_api_key`'s Revoke and Restore toasts name the key they acted on: a |
| 4 | +// record-level success message names the record (the write-feedback |
| 5 | +// convention). |
| 6 | +// |
| 7 | +// Both are `list_item` row actions. The console's runner fills a |
| 8 | +// `successMessage` `{{ record.* }}` hole from the row the dispatch carries |
| 9 | +// (ActionRunner `composeSuccessMessage`, pinned in objectui's |
| 10 | +// `ActionRunner.copyTemplate-13367.test.ts`, "the row a dispatch carries is |
| 11 | +// `record`"), so `{{ record.name }}` prints the key's name. A copy without the |
| 12 | +// hole names no key. |
| 13 | +// |
| 14 | +// The translated bundles are pinned as well: merge mode keeps every existing |
| 15 | +// translated value, so a revised source sentence would go on serving the old, |
| 16 | +// key-less translation, and both states are "in sync" to `check:i18n`. |
| 17 | +import { describe, expect, it } from 'vitest'; |
| 18 | +import { translateMetadataDocument } from '@objectstack/spec/system'; |
| 19 | +import { templateSlotSource } from '@objectstack/spec/automation'; |
| 20 | +import { SetupAppTranslations } from '../apps/translations/setup.translation.js'; |
| 21 | +import { enObjects } from '../apps/translations/en.objects.generated.js'; |
| 22 | +import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js'; |
| 23 | +import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; |
| 24 | +import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; |
| 25 | +import { SysApiKey } from './sys-api-key.object.js'; |
| 26 | + |
| 27 | +interface CopyAction { |
| 28 | + name?: string; |
| 29 | + successMessage?: unknown; |
| 30 | +} |
| 31 | + |
| 32 | +/** The hole the console runner fills from the dispatched row. */ |
| 33 | +const KEY_NAME = '{{ record.name }}'; |
| 34 | + |
| 35 | +const ACTIONS = ['revoke_api_key', 'restore_api_key'] as const; |
| 36 | + |
| 37 | +const TRANSLATED = [ |
| 38 | + ['zh-CN', zhCNObjects], |
| 39 | + ['ja-JP', jaJPObjects], |
| 40 | + ['es-ES', esESObjects], |
| 41 | +] as const; |
| 42 | + |
| 43 | +// `successMessage` is a `template` slot: the parsed object carries the |
| 44 | +// `{ dialect: 'template', source }` envelope, read through `templateSlotSource`. |
| 45 | +function action(object: unknown, name: string): CopyAction { |
| 46 | + const found = ((object as { actions?: CopyAction[] }).actions ?? []).find((a) => a.name === name); |
| 47 | + expect(found, `${name} is declared`).toBeDefined(); |
| 48 | + return { ...found, successMessage: templateSlotSource(found?.successMessage) }; |
| 49 | +} |
| 50 | + |
| 51 | +const bundleNode = (bundle: unknown, name: string): CopyAction | undefined => |
| 52 | + (bundle as Record<string, { _actions?: Record<string, CopyAction> }>)?.sys_api_key?._actions?.[name]; |
| 53 | + |
| 54 | +describe('sys_api_key Revoke / Restore — declared copy', () => { |
| 55 | + it.each(ACTIONS)('%s names the key in its success message', (name) => { |
| 56 | + expect(action(SysApiKey, name).successMessage).toContain(KEY_NAME); |
| 57 | + }); |
| 58 | + |
| 59 | + it('the two toasts are different sentences', () => { |
| 60 | + const [revoke, restore] = ACTIONS.map((name) => action(SysApiKey, name).successMessage); |
| 61 | + expect(revoke).not.toBe(restore); |
| 62 | + }); |
| 63 | +}); |
| 64 | + |
| 65 | +describe('sys_api_key Revoke / Restore — translated bundles follow the source', () => { |
| 66 | + it.each(ACTIONS)('en carries the source copy for %s', (name) => { |
| 67 | + expect(bundleNode(enObjects, name)?.successMessage).toBe(action(SysApiKey, name).successMessage); |
| 68 | + }); |
| 69 | + |
| 70 | + for (const [locale, bundle] of TRANSLATED) { |
| 71 | + it.each(ACTIONS)(`${locale} keeps the key-name token in the success message for %s`, (name) => { |
| 72 | + // The old translation survives every re-extract in merge mode; only the |
| 73 | + // token proves the leaf was revised with the source. |
| 74 | + const translated = bundleNode(bundle, name)?.successMessage; |
| 75 | + expect(translated, `${locale} sys_api_key.${name}.successMessage missing`).toBeTruthy(); |
| 76 | + expect(translated).toContain(KEY_NAME); |
| 77 | + expect(translated).not.toBe(action(SysApiKey, name).successMessage); |
| 78 | + }); |
| 79 | + } |
| 80 | +}); |
| 81 | + |
| 82 | +describe('sys_api_key Revoke / Restore — the served object metadata', () => { |
| 83 | + // The object-metadata read the console uses localizes through |
| 84 | + // `translateMetadataDocument('object', …)` over this package's bundle. |
| 85 | + const served = (locale: string, name: string): CopyAction => |
| 86 | + action(translateMetadataDocument('object', SysApiKey, SetupAppTranslations, { locale }), name); |
| 87 | + |
| 88 | + it.each(ACTIONS)('zh-CN serves the translated, key-naming copy for %s', (name) => { |
| 89 | + const zh = served('zh-CN', name); |
| 90 | + expect(zh.successMessage).toBe(bundleNode(zhCNObjects, name)?.successMessage); |
| 91 | + expect(zh.successMessage).toContain(KEY_NAME); |
| 92 | + }); |
| 93 | + |
| 94 | + it.each(ACTIONS)('en serves the declared copy for %s', (name) => { |
| 95 | + expect(served('en', name).successMessage).toBe(action(SysApiKey, name).successMessage); |
| 96 | + }); |
| 97 | +}); |
0 commit comments