@open-kingdom/shared-backend-integration-test-doubles 0.0.2-19 → 0.0.2-21
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/README.md +39 -0
- package/dist/apple-music/apple-music-error-mode.d.ts +17 -0
- package/dist/apple-music/apple-music-error-mode.d.ts.map +1 -0
- package/dist/apple-music/apple-music-error-mode.js +61 -0
- package/dist/apple-music/apple-music-error-mode.js.map +1 -0
- package/dist/apple-music/apple-music-twin.config.d.ts +9 -0
- package/dist/apple-music/apple-music-twin.config.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.config.js +25 -0
- package/dist/apple-music/apple-music-twin.config.js.map +1 -0
- package/dist/apple-music/apple-music-twin.d.ts +17 -0
- package/dist/apple-music/apple-music-twin.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.js +325 -0
- package/dist/apple-music/apple-music-twin.js.map +1 -0
- package/dist/apple-music/catalog-fixtures.d.ts +19 -0
- package/dist/apple-music/catalog-fixtures.d.ts.map +1 -0
- package/dist/apple-music/catalog-fixtures.js +94 -0
- package/dist/apple-music/catalog-fixtures.js.map +1 -0
- package/dist/apple-music/catalog-response.d.ts +59 -0
- package/dist/apple-music/catalog-response.d.ts.map +1 -0
- package/dist/apple-music/catalog-response.js +63 -0
- package/dist/apple-music/catalog-response.js.map +1 -0
- package/dist/apple-music/index.d.ts +8 -0
- package/dist/apple-music/index.d.ts.map +1 -0
- package/dist/apple-music/index.js +6 -0
- package/dist/apple-music/index.js.map +1 -0
- package/dist/apple-music/msw-handlers.d.ts +12 -0
- package/dist/apple-music/msw-handlers.d.ts.map +1 -0
- package/dist/apple-music/msw-handlers.js +18 -0
- package/dist/apple-music/msw-handlers.js.map +1 -0
- package/dist/apple-music/player-shim.d.ts +3 -0
- package/dist/apple-music/player-shim.d.ts.map +1 -0
- package/dist/apple-music/player-shim.js +310 -0
- package/dist/apple-music/player-shim.js.map +1 -0
- package/dist/artwork/playlist-001.svg +13 -0
- package/dist/artwork/playlist-002.svg +13 -0
- package/dist/artwork/track-001.svg +12 -0
- package/dist/artwork/track-002.svg +12 -0
- package/dist/artwork/track-003.svg +13 -0
- package/dist/artwork/track-004.svg +13 -0
- package/dist/artwork/track-005.svg +13 -0
- package/dist/artwork/track-006.svg +13 -0
- package/dist/artwork/track-007.svg +13 -0
- package/dist/audio/mock-1.mp3 +0 -0
- package/dist/audio/mock-2.mp3 +0 -0
- package/dist/audio/mock-3.mp3 +0 -0
- package/dist/gcs/gcs-error-mode.js +1 -1
- package/dist/gcs/gcs-error-mode.js.map +1 -1
- package/dist/gcs/gcs-error-simulation.interceptor.js.map +1 -1
- package/dist/gcs/gcs-twin.config.js.map +1 -1
- package/dist/gcs/gcs-twin.js +1 -1
- package/dist/gcs/gcs-twin.js.map +1 -1
- package/dist/gcs/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptor/http-helper.js.map +1 -1
- package/dist/interceptor/index.js.map +1 -1
- package/dist/interceptor/node-interceptor.js.map +1 -1
- package/dist/interceptor/routing-table.config.d.ts.map +1 -1
- package/dist/interceptor/routing-table.config.js +10 -0
- package/dist/interceptor/routing-table.config.js.map +1 -1
- package/dist/interceptor/routing-table.js +1 -1
- package/dist/interceptor/routing-table.js.map +1 -1
- package/dist/lib/integration-test-doubles.js.map +1 -1
- package/dist/shared/activation.js.map +1 -1
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/constants.d.ts +4 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +4 -2
- package/dist/shared/constants.js.map +1 -1
- package/dist/youtube/index.js.map +1 -1
- package/dist/youtube/msw-handlers.js.map +1 -1
- package/dist/youtube/player-shim.js.map +1 -1
- package/dist/youtube/search-fixtures.js.map +1 -1
- package/dist/youtube/search-response.js.map +1 -1
- package/dist/youtube/youtube-error-mode.js.map +1 -1
- package/dist/youtube/youtube-twin.config.js.map +1 -1
- package/dist/youtube/youtube-twin.js +5 -5
- package/dist/youtube/youtube-twin.js.map +1 -1
- package/package.json +23 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/interceptor/routing-table.config.ts"],"sourcesContent":["import { RoutingEntry } from './routing-table.js';\nimport { DEFAULT_PORTS } from '../shared/constants.js';\n\nexport const defaultRoutingEntries: RoutingEntry[] = [\n // Gmail twin\n {\n hostname: 'gmail.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n {\n hostname: 'oauth2.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n // YouTube twin — Data API\n {\n hostname: 'www.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n pathPrefix: '/youtube/',\n },\n // YouTube twin — IFrame API (browser CDN)\n {\n hostname: 'www.youtube.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n },\n];\n"],"names":["DEFAULT_PORTS","defaultRoutingEntries","hostname","target","GMAIL","YOUTUBE","pathPrefix"
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/routing-table.config.ts"],"sourcesContent":["import { RoutingEntry } from './routing-table.js';\nimport { DEFAULT_PORTS } from '../shared/constants.js';\n\nexport const defaultRoutingEntries: RoutingEntry[] = [\n // Gmail twin\n {\n hostname: 'gmail.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n {\n hostname: 'oauth2.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n // YouTube twin — Data API\n {\n hostname: 'www.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n pathPrefix: '/youtube/',\n },\n // YouTube twin — IFrame API (browser CDN)\n {\n hostname: 'www.youtube.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n },\n // Apple Music twin — Catalog API\n {\n hostname: 'api.music.apple.com',\n target: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`,\n },\n // Apple Music twin — Web SDK (browser CDN)\n {\n hostname: 'js-cdn.music.apple.com',\n target: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`,\n },\n];\n"],"names":["DEFAULT_PORTS","defaultRoutingEntries","hostname","target","GMAIL","YOUTUBE","pathPrefix","APPLE_MUSIC"],"mappings":"AACA,SAASA,aAAa,QAAQ,yBAAyB;AAEvD,OAAO,MAAMC,wBAAwC;IACnD,aAAa;IACb;QACEC,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcI,KAAK,EAAE;IACnD;IACA;QACEF,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcI,KAAK,EAAE;IACnD;IACA,0BAA0B;IAC1B;QACEF,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcK,OAAO,EAAE;QACnDC,YAAY;IACd;IACA,0CAA0C;IAC1C;QACEJ,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcK,OAAO,EAAE;IACrD;IACA,iCAAiC;IACjC;QACEH,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcO,WAAW,EAAE;IACzD;IACA,2CAA2C;IAC3C;QACEL,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcO,WAAW,EAAE;IACzD;CACD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/interceptor/routing-table.ts"],"sourcesContent":["export interface RoutingEntry {\n /** The target host to intercept (e.g., \"gmail.googleapis.com\") */\n hostname: string;\n /** The local target URL to rewrite requests to (e.g., \"http://localhost:9014\") */\n target: string;\n /** Optional path prefix restriction (only intercept paths starting with this) */\n pathPrefix?: string;\n}\n\nexport class RoutingTable {\n private readonly entries: Map<string, RoutingEntry[]> = new Map();\n\n constructor(initialEntries: RoutingEntry[] = []) {\n for (const entry of initialEntries) {\n this.addEntry(entry);\n }\n }\n\n addEntry(entry: RoutingEntry): void {\n const key = entry.hostname.toLowerCase();\n const list = this.entries.get(key) || [];\n list.push({\n ...entry,\n hostname: key,\n target: entry.target.replace(/\\/+$/, ''), // Strip trailing slashes safely\n });\n this.entries.set(key, list);\n }\n\n resolve(urlStr: string): string | null {\n try {\n const url = new URL(urlStr);\n const host = url.hostname.toLowerCase();\n const list = this.entries.get(host);\n if (!list) return null;\n\n for (const entry of list) {\n if (!entry.pathPrefix || url.pathname.startsWith(entry.pathPrefix)) {\n const rewritten = new URL(entry.target);\n // Set pathname correctly, preserving subpaths\n let targetPath = rewritten.pathname;\n if (targetPath === '/') {\n targetPath = '';\n }\n rewritten.pathname = targetPath + url.pathname;\n rewritten.search = url.search;\n return rewritten.toString();\n }\n }\n return null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["RoutingTable","addEntry","entry","key","hostname","toLowerCase","list","entries","get","push","target","replace","set","resolve","urlStr","url","URL","host","pathPrefix","pathname","startsWith","rewritten","targetPath","search","toString","
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/routing-table.ts"],"sourcesContent":["export interface RoutingEntry {\n /** The target host to intercept (e.g., \"gmail.googleapis.com\") */\n hostname: string;\n /** The local target URL to rewrite requests to (e.g., \"http://localhost:9014\") */\n target: string;\n /** Optional path prefix restriction (only intercept paths starting with this) */\n pathPrefix?: string;\n}\n\nexport class RoutingTable {\n private readonly entries: Map<string, RoutingEntry[]> = new Map();\n\n constructor(initialEntries: RoutingEntry[] = []) {\n for (const entry of initialEntries) {\n this.addEntry(entry);\n }\n }\n\n addEntry(entry: RoutingEntry): void {\n const key = entry.hostname.toLowerCase();\n const list = this.entries.get(key) || [];\n list.push({\n ...entry,\n hostname: key,\n target: entry.target.replace(/\\/+$/, ''), // Strip trailing slashes safely\n });\n this.entries.set(key, list);\n }\n\n resolve(urlStr: string): string | null {\n try {\n const url = new URL(urlStr);\n const host = url.hostname.toLowerCase();\n const list = this.entries.get(host);\n if (!list) return null;\n\n for (const entry of list) {\n if (!entry.pathPrefix || url.pathname.startsWith(entry.pathPrefix)) {\n const rewritten = new URL(entry.target);\n // Set pathname correctly, preserving subpaths\n let targetPath = rewritten.pathname;\n if (targetPath === '/') {\n targetPath = '';\n }\n rewritten.pathname = targetPath + url.pathname;\n rewritten.search = url.search;\n return rewritten.toString();\n }\n }\n return null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["RoutingTable","addEntry","entry","key","hostname","toLowerCase","list","entries","get","push","target","replace","set","resolve","urlStr","url","URL","host","pathPrefix","pathname","startsWith","rewritten","targetPath","search","toString","initialEntries","Map"],"mappings":";AASA,OAAO,MAAMA;IASXC,SAASC,KAAmB,EAAQ;QAClC,MAAMC,MAAMD,MAAME,QAAQ,CAACC,WAAW;QACtC,MAAMC,OAAO,IAAI,CAACC,OAAO,CAACC,GAAG,CAACL,QAAQ,EAAE;QACxCG,KAAKG,IAAI,CAAC,aACLP;YACHE,UAAUD;YACVO,QAAQR,MAAMQ,MAAM,CAACC,OAAO,CAAC,QAAQ;;QAEvC,IAAI,CAACJ,OAAO,CAACK,GAAG,CAACT,KAAKG;IACxB;IAEAO,QAAQC,MAAc,EAAiB;QACrC,IAAI;YACF,MAAMC,MAAM,IAAIC,IAAIF;YACpB,MAAMG,OAAOF,IAAIX,QAAQ,CAACC,WAAW;YACrC,MAAMC,OAAO,IAAI,CAACC,OAAO,CAACC,GAAG,CAACS;YAC9B,IAAI,CAACX,MAAM,OAAO;YAElB,KAAK,MAAMJ,SAASI,KAAM;gBACxB,IAAI,CAACJ,MAAMgB,UAAU,IAAIH,IAAII,QAAQ,CAACC,UAAU,CAAClB,MAAMgB,UAAU,GAAG;oBAClE,MAAMG,YAAY,IAAIL,IAAId,MAAMQ,MAAM;oBACtC,8CAA8C;oBAC9C,IAAIY,aAAaD,UAAUF,QAAQ;oBACnC,IAAIG,eAAe,KAAK;wBACtBA,aAAa;oBACf;oBACAD,UAAUF,QAAQ,GAAGG,aAAaP,IAAII,QAAQ;oBAC9CE,UAAUE,MAAM,GAAGR,IAAIQ,MAAM;oBAC7B,OAAOF,UAAUG,QAAQ;gBAC3B;YACF;YACA,OAAO;QACT,EAAE,eAAM;YACN,OAAO;QACT;IACF;IAzCA,YAAYC,iBAAiC,EAAE,CAAE;aAFhClB,UAAuC,IAAImB;QAG1D,KAAK,MAAMxB,SAASuB,eAAgB;YAClC,IAAI,CAACxB,QAAQ,CAACC;QAChB;IACF;AAsCF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/integration-test-doubles.ts"],"sourcesContent":["export function integrationTestDoubles(): string {\n return 'integration-test-doubles';\n}\n"],"names":["integrationTestDoubles"],"
|
|
1
|
+
{"version":3,"sources":["../../src/lib/integration-test-doubles.ts"],"sourcesContent":["export function integrationTestDoubles(): string {\n return 'integration-test-doubles';\n}\n"],"names":["integrationTestDoubles"],"mappings":"AAAA,OAAO,SAASA;IACd,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/activation.ts"],"sourcesContent":["/**\n * Activation gate for integration‑test‑doubles.\n * Twins should only start when the test‑mode environment variable is set.\n */\n\n/**\n * Returns `true` if the library is running in test mode.\n *\n * Test mode is enabled when the environment variable `TEST_MODE` is exactly the string `'true'`.\n * This check is case‑sensitive.\n *\n * @example\n * ```ts\n * if (isTestMode()) {\n * await twin.start();\n * }\n * ```\n */\nexport function isTestMode(): boolean {\n return process.env.TEST_MODE === 'true';\n}\n"],"names":["isTestMode","process","env","TEST_MODE"],"
|
|
1
|
+
{"version":3,"sources":["../../src/shared/activation.ts"],"sourcesContent":["/**\n * Activation gate for integration‑test‑doubles.\n * Twins should only start when the test‑mode environment variable is set.\n */\n\n/**\n * Returns `true` if the library is running in test mode.\n *\n * Test mode is enabled when the environment variable `TEST_MODE` is exactly the string `'true'`.\n * This check is case‑sensitive.\n *\n * @example\n * ```ts\n * if (isTestMode()) {\n * await twin.start();\n * }\n * ```\n */\nexport function isTestMode(): boolean {\n return process.env.TEST_MODE === 'true';\n}\n"],"names":["isTestMode","process","env","TEST_MODE"],"mappings":"AAAA;;;CAGC,GAED;;;;;;;;;;;;CAYC,GACD,OAAO,SAASA;IACd,OAAOC,QAAQC,GAAG,CAACC,SAAS,KAAK;AACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/config.ts"],"sourcesContent":["import {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from './constants.js';\n\n/**\n * Configuration for a single bucket to create on startup.\n */\nexport interface BucketConfig {\n /** Bucket name */\n name: string;\n}\n\n/**\n * Configuration for the GCS twin.\n */\nexport interface GcsTwinConfig {\n /** Port the fake‑gcs‑server listens on */\n port: number;\n /** External URL the SDK will connect to (e.g., http://localhost:9013) */\n externalUrl: string;\n /** Optional persistent data directory (if not set, data is ephemeral) */\n dataDir?: string;\n /** Buckets to create on startup */\n buckets: BucketConfig[];\n}\n\n/**\n * Default configuration for the GCS twin.\n *\n * Values are derived from the implementation guide (docs/replication‑engine/twins/01‑gcs‑twin.md).\n */\nexport const defaultGcsConfig: GcsTwinConfig = {\n port: DEFAULT_PORTS.GCS,\n externalUrl: `http://localhost:${DEFAULT_PORTS.GCS}`,\n buckets: [{ name: 'app-assets' }, { name: 'user-uploads' }],\n};\n\n/**\n * Environment‑variable keys that affect GCS twin configuration.\n */\nconst _CONFIG_ENV_KEYS = [\n ENV_VARS.GCS_TWIN_PORT,\n ENV_VARS.GCS_TWIN_DATA_DIR,\n] as const;\n\n/**\n * Creates a typed ConfigService for GCS twin environment variables.\n */\nfunction createEnvConfigService(): ConfigService<\n (typeof _CONFIG_ENV_KEYS)[number]\n> {\n return new ConfigService(nodeEnvAdapter);\n}\n\n/**\n * Parses a string environment variable as a number.\n * Returns undefined if the variable is not set or cannot be parsed.\n */\nfunction parseOptionalInt(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * Creates a GcsTwinConfig by merging defaults, environment variables, and explicit overrides.\n *\n * Environment variables take precedence over defaults.\n * Explicit overrides (passed as argument) take precedence over environment variables.\n *\n * @param overrides - Partial configuration that overrides both defaults and environment variables.\n * @returns A complete GcsTwinConfig.\n */\nexport function createGcsConfig(\n overrides: Partial<GcsTwinConfig> = {}\n): GcsTwinConfig {\n const env = createEnvConfigService();\n\n // Environment‑variable overrides\n const envPort = parseOptionalInt(env.get(ENV_VARS.GCS_TWIN_PORT));\n const envDataDir = env.get(ENV_VARS.GCS_TWIN_DATA_DIR);\n\n const baseConfig: GcsTwinConfig = {\n ...defaultGcsConfig,\n ...(envPort !== undefined && { port: envPort }),\n ...(envDataDir !== undefined && { dataDir: envDataDir }),\n };\n\n // Explicit overrides take final precedence\n const finalConfig: GcsTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n // Validate port is within reserved range\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `GCS twin port ${finalConfig.port} is outside the reserved range ${PORT_RANGE.min}‑${PORT_RANGE.max}. ` +\n `Adjust PORT_RANGE in constants.ts or set ${ENV_VARS.GCS_TWIN_PORT} appropriately.`\n );\n }\n\n // Ensure externalUrl matches the port (unless explicitly overridden)\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultGcsConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","defaultGcsConfig","port","GCS","externalUrl","buckets","name","_CONFIG_ENV_KEYS","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","createEnvConfigService","parseOptionalInt","value","undefined","parsed","Number","parseInt","isNaN","createGcsConfig","overrides","env","envPort","get","envDataDir","baseConfig","dataDir","finalConfig","min","max","Error"],"
|
|
1
|
+
{"version":3,"sources":["../../src/shared/config.ts"],"sourcesContent":["import {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from './constants.js';\n\n/**\n * Configuration for a single bucket to create on startup.\n */\nexport interface BucketConfig {\n /** Bucket name */\n name: string;\n}\n\n/**\n * Configuration for the GCS twin.\n */\nexport interface GcsTwinConfig {\n /** Port the fake‑gcs‑server listens on */\n port: number;\n /** External URL the SDK will connect to (e.g., http://localhost:9013) */\n externalUrl: string;\n /** Optional persistent data directory (if not set, data is ephemeral) */\n dataDir?: string;\n /** Buckets to create on startup */\n buckets: BucketConfig[];\n}\n\n/**\n * Default configuration for the GCS twin.\n *\n * Values are derived from the implementation guide (docs/replication‑engine/twins/01‑gcs‑twin.md).\n */\nexport const defaultGcsConfig: GcsTwinConfig = {\n port: DEFAULT_PORTS.GCS,\n externalUrl: `http://localhost:${DEFAULT_PORTS.GCS}`,\n buckets: [{ name: 'app-assets' }, { name: 'user-uploads' }],\n};\n\n/**\n * Environment‑variable keys that affect GCS twin configuration.\n */\nconst _CONFIG_ENV_KEYS = [\n ENV_VARS.GCS_TWIN_PORT,\n ENV_VARS.GCS_TWIN_DATA_DIR,\n] as const;\n\n/**\n * Creates a typed ConfigService for GCS twin environment variables.\n */\nfunction createEnvConfigService(): ConfigService<\n (typeof _CONFIG_ENV_KEYS)[number]\n> {\n return new ConfigService(nodeEnvAdapter);\n}\n\n/**\n * Parses a string environment variable as a number.\n * Returns undefined if the variable is not set or cannot be parsed.\n */\nfunction parseOptionalInt(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * Creates a GcsTwinConfig by merging defaults, environment variables, and explicit overrides.\n *\n * Environment variables take precedence over defaults.\n * Explicit overrides (passed as argument) take precedence over environment variables.\n *\n * @param overrides - Partial configuration that overrides both defaults and environment variables.\n * @returns A complete GcsTwinConfig.\n */\nexport function createGcsConfig(\n overrides: Partial<GcsTwinConfig> = {}\n): GcsTwinConfig {\n const env = createEnvConfigService();\n\n // Environment‑variable overrides\n const envPort = parseOptionalInt(env.get(ENV_VARS.GCS_TWIN_PORT));\n const envDataDir = env.get(ENV_VARS.GCS_TWIN_DATA_DIR);\n\n const baseConfig: GcsTwinConfig = {\n ...defaultGcsConfig,\n ...(envPort !== undefined && { port: envPort }),\n ...(envDataDir !== undefined && { dataDir: envDataDir }),\n };\n\n // Explicit overrides take final precedence\n const finalConfig: GcsTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n // Validate port is within reserved range\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `GCS twin port ${finalConfig.port} is outside the reserved range ${PORT_RANGE.min}‑${PORT_RANGE.max}. ` +\n `Adjust PORT_RANGE in constants.ts or set ${ENV_VARS.GCS_TWIN_PORT} appropriately.`\n );\n }\n\n // Ensure externalUrl matches the port (unless explicitly overridden)\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultGcsConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","defaultGcsConfig","port","GCS","externalUrl","buckets","name","_CONFIG_ENV_KEYS","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","createEnvConfigService","parseOptionalInt","value","undefined","parsed","Number","parseInt","isNaN","createGcsConfig","overrides","env","envPort","get","envDataDir","baseConfig","dataDir","finalConfig","min","max","Error"],"mappings":";AAAA,SACEA,aAAa,EACbC,cAAc,QACT,4CAA4C;AACnD,SAASC,aAAa,EAAEC,QAAQ,EAAEC,UAAU,QAAQ,iBAAiB;AAwBrE;;;;CAIC,GACD,OAAO,MAAMC,mBAAkC;IAC7CC,MAAMJ,cAAcK,GAAG;IACvBC,aAAa,CAAC,iBAAiB,EAAEN,cAAcK,GAAG,EAAE;IACpDE,SAAS;QAAC;YAAEC,MAAM;QAAa;QAAG;YAAEA,MAAM;QAAe;KAAE;AAC7D,EAAE;AAEF;;CAEC,GACD,MAAMC,mBAAmB;IACvBR,SAASS,aAAa;IACtBT,SAASU,iBAAiB;CAC3B;AAED;;CAEC,GACD,SAASC;IAGP,OAAO,IAAId,cAAcC;AAC3B;AAEA;;;CAGC,GACD,SAASc,iBAAiBC,KAAyB;IACjD,IAAIA,UAAUC,WAAW,OAAOA;IAChC,MAAMC,SAASC,OAAOC,QAAQ,CAACJ,OAAO;IACtC,OAAOG,OAAOE,KAAK,CAACH,UAAUD,YAAYC;AAC5C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,gBACdC,YAAoC,CAAC,CAAC;IAEtC,MAAMC,MAAMV;IAEZ,iCAAiC;IACjC,MAAMW,UAAUV,iBAAiBS,IAAIE,GAAG,CAACvB,SAASS,aAAa;IAC/D,MAAMe,aAAaH,IAAIE,GAAG,CAACvB,SAASU,iBAAiB;IAErD,MAAMe,aAA4B,aAC7BvB,kBACCoB,YAAYR,aAAa;QAAEX,MAAMmB;IAAQ,GACzCE,eAAeV,aAAa;QAAEY,SAASF;IAAW;IAGxD,2CAA2C;IAC3C,MAAMG,cAA6B,aAC9BF,YACAL;IAGL,yCAAyC;IACzC,IAAIO,YAAYxB,IAAI,GAAGF,WAAW2B,GAAG,IAAID,YAAYxB,IAAI,GAAGF,WAAW4B,GAAG,EAAE;QAC1E,MAAM,IAAIC,MACR,CAAC,cAAc,EAAEH,YAAYxB,IAAI,CAAC,+BAA+B,EAAEF,WAAW2B,GAAG,CAAC,CAAC,EAAE3B,WAAW4B,GAAG,CAAC,EAAE,CAAC,GACrG,CAAC,yCAAyC,EAAE7B,SAASS,aAAa,CAAC,eAAe,CAAC;IAEzF;IAEA,qEAAqE;IACrE,IACE,CAACW,UAAUf,WAAW,IACtBsB,YAAYtB,WAAW,KAAKH,iBAAiBG,WAAW,EACxD;QACAsB,YAAYtB,WAAW,GAAG,CAAC,iBAAiB,EAAEsB,YAAYxB,IAAI,EAAE;IAClE;IAEA,OAAOwB;AACT"}
|
|
@@ -25,6 +25,8 @@ export declare const DEFAULT_PORTS: {
|
|
|
25
25
|
readonly GOOGLE_CALENDAR: 9017;
|
|
26
26
|
/** Spotify twin (future) */
|
|
27
27
|
readonly SPOTIFY: 9018;
|
|
28
|
+
/** Apple Music twin */
|
|
29
|
+
readonly APPLE_MUSIC: 9019;
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* Environment variable names used by the library.
|
|
@@ -40,5 +42,7 @@ export declare const ENV_VARS: {
|
|
|
40
42
|
readonly GMAIL_TWIN_PORT: "GMAIL_TWIN_PORT";
|
|
41
43
|
/** YouTube twin port override */
|
|
42
44
|
readonly YOUTUBE_TWIN_PORT: "YOUTUBE_TWIN_PORT";
|
|
45
|
+
/** Apple Music twin port override */
|
|
46
|
+
readonly APPLE_MUSIC_TWIN_PORT: "APPLE_MUSIC_TWIN_PORT";
|
|
43
47
|
};
|
|
44
48
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,gCAAgC;;IAEhC,iDAAiD;;IAEjD,gCAAgC;;IAEhC,4BAA4B;;IAE5B,oCAAoC;;IAEpC,4BAA4B;;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,gCAAgC;;IAEhC,iDAAiD;;IAEjD,gCAAgC;;IAEhC,4BAA4B;;IAE5B,oCAAoC;;IAEpC,4BAA4B;;IAE5B,uBAAuB;;CAEf,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;IACnB,0DAA0D;;IAE1D,6BAA6B;;IAE7B,oDAAoD;;IAEpD,+BAA+B;;IAE/B,iCAAiC;;IAEjC,qCAAqC;;CAE7B,CAAC"}
|
package/dist/shared/constants.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
/** Google Auth twin (future) */ GOOGLE_AUTH: 9015,
|
|
16
16
|
/** YouTube twin (future) */ YOUTUBE: 9016,
|
|
17
17
|
/** Google Calendar twin (future) */ GOOGLE_CALENDAR: 9017,
|
|
18
|
-
/** Spotify twin (future) */ SPOTIFY: 9018
|
|
18
|
+
/** Spotify twin (future) */ SPOTIFY: 9018,
|
|
19
|
+
/** Apple Music twin */ APPLE_MUSIC: 9019
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
22
|
* Environment variable names used by the library.
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
/** GCS twin port override */ GCS_TWIN_PORT: 'GCS_TWIN_PORT',
|
|
25
26
|
/** GCS twin persistent data directory (optional) */ GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',
|
|
26
27
|
/** Gmail twin port override */ GMAIL_TWIN_PORT: 'GMAIL_TWIN_PORT',
|
|
27
|
-
/** YouTube twin port override */ YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT'
|
|
28
|
+
/** YouTube twin port override */ YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT',
|
|
29
|
+
/** Apple Music twin port override */ APPLE_MUSIC_TWIN_PORT: 'APPLE_MUSIC_TWIN_PORT'
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Shared constants for integration‑test‑doubles library.\n */\n\n/**\n * Port range reserved for twin emulators.\n * Twins should use fixed ports within this range to avoid collisions.\n */\nexport const PORT_RANGE = {\n min: 9010,\n max: 9020,\n} as const;\n\n/**\n * Default port assignments for each twin.\n */\nexport const DEFAULT_PORTS = {\n /** Google Cloud Storage twin */\n GCS: 9013,\n /** Gmail/Google Workspace Email twin (future) */\n GMAIL: 9014,\n /** Google Auth twin (future) */\n GOOGLE_AUTH: 9015,\n /** YouTube twin (future) */\n YOUTUBE: 9016,\n /** Google Calendar twin (future) */\n GOOGLE_CALENDAR: 9017,\n /** Spotify twin (future) */\n SPOTIFY: 9018,\n} as const;\n\n/**\n * Environment variable names used by the library.\n */\nexport const ENV_VARS = {\n /** Activation gate — must be 'true' for twins to start */\n TEST_MODE: 'TEST_MODE',\n /** GCS twin port override */\n GCS_TWIN_PORT: 'GCS_TWIN_PORT',\n /** GCS twin persistent data directory (optional) */\n GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',\n /** Gmail twin port override */\n GMAIL_TWIN_PORT: 'GMAIL_TWIN_PORT',\n /** YouTube twin port override */\n YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT',\n} as const;\n"],"names":["PORT_RANGE","min","max","DEFAULT_PORTS","GCS","GMAIL","GOOGLE_AUTH","YOUTUBE","GOOGLE_CALENDAR","SPOTIFY","ENV_VARS","TEST_MODE","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","GMAIL_TWIN_PORT","YOUTUBE_TWIN_PORT"
|
|
1
|
+
{"version":3,"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Shared constants for integration‑test‑doubles library.\n */\n\n/**\n * Port range reserved for twin emulators.\n * Twins should use fixed ports within this range to avoid collisions.\n */\nexport const PORT_RANGE = {\n min: 9010,\n max: 9020,\n} as const;\n\n/**\n * Default port assignments for each twin.\n */\nexport const DEFAULT_PORTS = {\n /** Google Cloud Storage twin */\n GCS: 9013,\n /** Gmail/Google Workspace Email twin (future) */\n GMAIL: 9014,\n /** Google Auth twin (future) */\n GOOGLE_AUTH: 9015,\n /** YouTube twin (future) */\n YOUTUBE: 9016,\n /** Google Calendar twin (future) */\n GOOGLE_CALENDAR: 9017,\n /** Spotify twin (future) */\n SPOTIFY: 9018,\n /** Apple Music twin */\n APPLE_MUSIC: 9019,\n} as const;\n\n/**\n * Environment variable names used by the library.\n */\nexport const ENV_VARS = {\n /** Activation gate — must be 'true' for twins to start */\n TEST_MODE: 'TEST_MODE',\n /** GCS twin port override */\n GCS_TWIN_PORT: 'GCS_TWIN_PORT',\n /** GCS twin persistent data directory (optional) */\n GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',\n /** Gmail twin port override */\n GMAIL_TWIN_PORT: 'GMAIL_TWIN_PORT',\n /** YouTube twin port override */\n YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT',\n /** Apple Music twin port override */\n APPLE_MUSIC_TWIN_PORT: 'APPLE_MUSIC_TWIN_PORT',\n} as const;\n"],"names":["PORT_RANGE","min","max","DEFAULT_PORTS","GCS","GMAIL","GOOGLE_AUTH","YOUTUBE","GOOGLE_CALENDAR","SPOTIFY","APPLE_MUSIC","ENV_VARS","TEST_MODE","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","GMAIL_TWIN_PORT","YOUTUBE_TWIN_PORT","APPLE_MUSIC_TWIN_PORT"],"mappings":"AAAA;;CAEC,GAED;;;CAGC,GACD,OAAO,MAAMA,aAAa;IACxBC,KAAK;IACLC,KAAK;AACP,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,gBAAgB;IAC3B,8BAA8B,GAC9BC,KAAK;IACL,+CAA+C,GAC/CC,OAAO;IACP,8BAA8B,GAC9BC,aAAa;IACb,0BAA0B,GAC1BC,SAAS;IACT,kCAAkC,GAClCC,iBAAiB;IACjB,0BAA0B,GAC1BC,SAAS;IACT,qBAAqB,GACrBC,aAAa;AACf,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,WAAW;IACtB,wDAAwD,GACxDC,WAAW;IACX,2BAA2B,GAC3BC,eAAe;IACf,kDAAkD,GAClDC,mBAAmB;IACnB,6BAA6B,GAC7BC,iBAAiB;IACjB,+BAA+B,GAC/BC,mBAAmB;IACnB,mCAAmC,GACnCC,uBAAuB;AACzB,EAAW"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/index.ts"],"sourcesContent":["/**\n * Public API for the YouTube twin.\n */\nexport { YoutubeTwin } from './youtube-twin.js';\nexport type { YoutubeTwinConfig } from './youtube-twin.config.js';\nexport {\n defaultYoutubeConfig,\n createYoutubeConfig,\n} from './youtube-twin.config.js';\nexport { searchFixtures } from './search-fixtures.js';\nexport type { VideoFixture } from './search-fixtures.js';\nexport { formatSearchResponse } from './search-response.js';\nexport { generatePlayerShim } from './player-shim.js';\nexport { YoutubeErrorModeManager } from './youtube-error-mode.js';\nexport type {\n YoutubeErrorModeType,\n YoutubeApiErrorModeType,\n YoutubePlayerErrorCode,\n YoutubeErrorModeConfig,\n YoutubeErrorSimulationResult,\n} from './youtube-error-mode.js';\nexport { getYoutubeMswHandlerConfigs } from './msw-handlers.js';\nexport type { MswHandlerConfig } from './msw-handlers.js';\n"],"names":["YoutubeTwin","defaultYoutubeConfig","createYoutubeConfig","searchFixtures","formatSearchResponse","generatePlayerShim","YoutubeErrorModeManager","getYoutubeMswHandlerConfigs"],"
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/index.ts"],"sourcesContent":["/**\n * Public API for the YouTube twin.\n */\nexport { YoutubeTwin } from './youtube-twin.js';\nexport type { YoutubeTwinConfig } from './youtube-twin.config.js';\nexport {\n defaultYoutubeConfig,\n createYoutubeConfig,\n} from './youtube-twin.config.js';\nexport { searchFixtures } from './search-fixtures.js';\nexport type { VideoFixture } from './search-fixtures.js';\nexport { formatSearchResponse } from './search-response.js';\nexport { generatePlayerShim } from './player-shim.js';\nexport { YoutubeErrorModeManager } from './youtube-error-mode.js';\nexport type {\n YoutubeErrorModeType,\n YoutubeApiErrorModeType,\n YoutubePlayerErrorCode,\n YoutubeErrorModeConfig,\n YoutubeErrorSimulationResult,\n} from './youtube-error-mode.js';\nexport { getYoutubeMswHandlerConfigs } from './msw-handlers.js';\nexport type { MswHandlerConfig } from './msw-handlers.js';\n"],"names":["YoutubeTwin","defaultYoutubeConfig","createYoutubeConfig","searchFixtures","formatSearchResponse","generatePlayerShim","YoutubeErrorModeManager","getYoutubeMswHandlerConfigs"],"mappings":"AAAA;;CAEC,GACD,SAASA,WAAW,QAAQ,oBAAoB;AAEhD,SACEC,oBAAoB,EACpBC,mBAAmB,QACd,2BAA2B;AAClC,SAASC,cAAc,QAAQ,uBAAuB;AAEtD,SAASC,oBAAoB,QAAQ,uBAAuB;AAC5D,SAASC,kBAAkB,QAAQ,mBAAmB;AACtD,SAASC,uBAAuB,QAAQ,0BAA0B;AAQlE,SAASC,2BAA2B,QAAQ,oBAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/msw-handlers.ts"],"sourcesContent":["/**\n * MSW handler configuration for YouTube Twin browser-side interception.\n *\n * Consumers install these handlers in their MSW worker setup.\n * The handler factory does NOT import MSW — it returns plain objects\n * that consumers use with their own MSW installation.\n *\n * Usage in consuming app:\n *\n * ```typescript\n * import { http, HttpResponse } from 'msw';\n * import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';\n *\n * const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');\n *\n * const handlers = configs.map(config =>\n * http.get(config.url, async () => {\n * const response = await fetch(config.proxyTo);\n * const body = await response.text();\n * return new HttpResponse(body, {\n * headers: { 'Content-Type': config.contentType },\n * });\n * })\n * );\n *\n * worker.start({ handlers });\n * ```\n */\n\nexport interface MswHandlerConfig {\n /** The original URL to intercept */\n url: string;\n /** The local twin URL to proxy to */\n proxyTo: string;\n /** Content-Type header for the response */\n contentType: string;\n /** Description for debugging */\n description: string;\n}\n\nexport function getYoutubeMswHandlerConfigs(\n twinBaseUrl = 'http://localhost:9016'\n): MswHandlerConfig[] {\n return [\n {\n url: 'https://www.youtube.com/iframe_api',\n proxyTo: `${twinBaseUrl}/iframe_api`,\n contentType: 'application/javascript',\n description: 'YouTube IFrame Player API shim',\n },\n {\n url: 'https://www.youtube.com/s/player/*/www-widgetapi.vflset/www-widgetapi.js',\n proxyTo: `${twinBaseUrl}/shim/youtube-player.js`,\n contentType: 'application/javascript',\n description: 'YouTube widget API (secondary load)',\n },\n ];\n}\n"],"names":["getYoutubeMswHandlerConfigs","twinBaseUrl","url","proxyTo","contentType","description"],"
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/msw-handlers.ts"],"sourcesContent":["/**\n * MSW handler configuration for YouTube Twin browser-side interception.\n *\n * Consumers install these handlers in their MSW worker setup.\n * The handler factory does NOT import MSW — it returns plain objects\n * that consumers use with their own MSW installation.\n *\n * Usage in consuming app:\n *\n * ```typescript\n * import { http, HttpResponse } from 'msw';\n * import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';\n *\n * const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');\n *\n * const handlers = configs.map(config =>\n * http.get(config.url, async () => {\n * const response = await fetch(config.proxyTo);\n * const body = await response.text();\n * return new HttpResponse(body, {\n * headers: { 'Content-Type': config.contentType },\n * });\n * })\n * );\n *\n * worker.start({ handlers });\n * ```\n */\n\nexport interface MswHandlerConfig {\n /** The original URL to intercept */\n url: string;\n /** The local twin URL to proxy to */\n proxyTo: string;\n /** Content-Type header for the response */\n contentType: string;\n /** Description for debugging */\n description: string;\n}\n\nexport function getYoutubeMswHandlerConfigs(\n twinBaseUrl = 'http://localhost:9016'\n): MswHandlerConfig[] {\n return [\n {\n url: 'https://www.youtube.com/iframe_api',\n proxyTo: `${twinBaseUrl}/iframe_api`,\n contentType: 'application/javascript',\n description: 'YouTube IFrame Player API shim',\n },\n {\n url: 'https://www.youtube.com/s/player/*/www-widgetapi.vflset/www-widgetapi.js',\n proxyTo: `${twinBaseUrl}/shim/youtube-player.js`,\n contentType: 'application/javascript',\n description: 'YouTube widget API (secondary load)',\n },\n ];\n}\n"],"names":["getYoutubeMswHandlerConfigs","twinBaseUrl","url","proxyTo","contentType","description"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAaD,OAAO,SAASA,4BACdC,cAAc,uBAAuB;IAErC,OAAO;QACL;YACEC,KAAK;YACLC,SAAS,GAAGF,YAAY,WAAW,CAAC;YACpCG,aAAa;YACbC,aAAa;QACf;QACA;YACEH,KAAK;YACLC,SAAS,GAAGF,YAAY,uBAAuB,CAAC;YAChDG,aAAa;YACbC,aAAa;QACf;KACD;AACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/player-shim.ts"],"sourcesContent":["export const SHIM_TEMPLATE = `(function() {\n 'use strict';\n\n var TWIN_BASE = '__TWIN_BASE_URL__';\n\n var PlayerState = {\n UNSTARTED: -1,\n ENDED: 0,\n PLAYING: 1,\n PAUSED: 2,\n BUFFERING: 3,\n CUED: 5\n };\n\n function Player(elementIdOrElement, options) {\n options = options || {};\n\n // Resolve target container\n this._container = typeof elementIdOrElement === 'string'\n ? document.getElementById(elementIdOrElement)\n : elementIdOrElement;\n\n if (!this._container) {\n throw new Error('YT.Player: target element not found: ' + elementIdOrElement);\n }\n\n this._options = options;\n this._state = PlayerState.UNSTARTED;\n this._volume = 100;\n this._muted = false;\n this._hasError = false;\n this._errorCode = null;\n this._videoData = {\n video_id: options.videoId || '',\n title: '',\n author: ''\n };\n\n // Create HTML5 <video> element\n this._video = document.createElement('video');\n this._video.src = TWIN_BASE + '/test-assets/sample.mp4';\n this._video.width = options.width || 640;\n this._video.height = options.height || 360;\n this._video.controls = !!(options.playerVars && options.playerVars.controls !== 0);\n this._video.playsInline = !!(options.playerVars && options.playerVars.playsinline);\n this._video.style.width = '100%';\n this._video.style.height = '100%';\n this._video.style.backgroundColor = '#000';\n this._video.style.display = 'block';\n\n // Override video.play to check error state\n var self = this;\n var originalPlay = this._video.play;\n this._video.play = function() {\n if (self._hasError) {\n return Promise.reject(new Error('Playback blocked due to player error ' + self._errorCode));\n }\n return originalPlay.apply(this, arguments);\n };\n\n // Create metadata overlay\n this._overlay = document.createElement('div');\n this._overlay.style.cssText =\n 'position:absolute;bottom:0;left:0;right:0;padding:8px 12px;' +\n 'background:linear-gradient(transparent, rgba(0,0,0,0.8));' +\n 'color:#fff;font-size:14px;font-family:Roboto,Arial,sans-serif;' +\n 'pointer-events:none;z-index:10;';\n this._loadVideoMetadata(options.videoId);\n\n // Wrap in positioned container\n this._wrapper = document.createElement('div');\n this._wrapper.style.cssText = 'position:relative;overflow:hidden;width:' + (options.width ? options.width + 'px' : '100%') + ';height:' + (options.height ? options.height + 'px' : '100%') + ';';\n this._wrapper.appendChild(this._video);\n this._wrapper.appendChild(this._overlay);\n\n // Replace target element content\n this._container.innerHTML = '';\n this._container.appendChild(this._wrapper);\n\n // Wire HTML5 video events -> YT state changes\n this._video.addEventListener('play', function() {\n if (self._hasError) {\n self._video.pause();\n }\n });\n this._video.addEventListener('playing', function() {\n if (self._hasError) {\n self._video.pause();\n return;\n }\n self._state = PlayerState.PLAYING;\n self._fireStateChange();\n });\n this._video.addEventListener('pause', function() {\n if (self._video.ended) return; // 'ended' fires separately\n if (self._hasError) return;\n self._state = PlayerState.PAUSED;\n self._fireStateChange();\n });\n this._video.addEventListener('ended', function() {\n if (self._hasError) return;\n self._state = PlayerState.ENDED;\n self._fireStateChange();\n });\n this._video.addEventListener('waiting', function() {\n if (self._hasError) return;\n self._state = PlayerState.BUFFERING;\n self._fireStateChange();\n });\n\n // Fire onReady asynchronously (matches real YT behavior)\n setTimeout(function() {\n if (options.events && typeof options.events.onReady === 'function') {\n options.events.onReady({ target: self });\n }\n }, 100);\n\n }\n\n Player.prototype._loadVideoMetadata = function(videoId) {\n if (!videoId) {\n this._overlay.textContent = 'YouTube Twin Player';\n return;\n }\n\n var self = this;\n\n // Check error-mode control endpoint first\n fetch(TWIN_BASE + '/test/youtube/error-mode')\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data.active && data.mode && data.mode.startsWith('player-error-')) {\n var code = parseInt(data.mode.split('-')[2], 10);\n setTimeout(function() {\n self._fireError(code);\n }, 200);\n return;\n }\n\n // Auto-play if configured and no error\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n\n // Fetch actual mock metadata\n fetch(TWIN_BASE + '/youtube/v3/search?q=' + encodeURIComponent(videoId) +\n '&type=video&maxResults=1&key=shim&part=snippet')\n .then(function(res) {\n if (!res.ok) throw new Error('Search failed');\n return res.json();\n })\n .then(function(searchData) {\n if (searchData.items && searchData.items.length > 0) {\n var snippet = searchData.items[0].snippet;\n self._videoData = {\n video_id: videoId,\n title: snippet.title,\n author: snippet.channelTitle\n };\n self._overlay.textContent = snippet.title + ' - ' + snippet.channelTitle;\n } else {\n self._overlay.textContent = 'Video: ' + videoId;\n }\n })\n .catch(function() {\n self._overlay.textContent = 'Video: ' + videoId;\n });\n })\n .catch(function() {\n // Auto-play if configured and endpoint query fails\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n self._overlay.textContent = 'Video: ' + videoId;\n });\n };\n\n Player.prototype._fireStateChange = function() {\n if (this._options.events && typeof this._options.events.onStateChange === 'function') {\n this._options.events.onStateChange({ target: this, data: this._state });\n }\n };\n\n Player.prototype._fireError = function(code) {\n this._hasError = true;\n this._errorCode = code;\n this._state = PlayerState.UNSTARTED;\n\n // Pause and clean up video\n if (this._video) {\n this._video.pause();\n this._video.src = '';\n try {\n this._video.removeAttribute('src');\n this._video.load();\n } catch (e) {}\n }\n\n var desc = 'An unknown player error occurred.';\n if (code === 2) desc = 'The request contains an invalid parameter value.';\n else if (code === 5) desc = 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';\n else if (code === 100) desc = 'The video requested was not found (removed or marked as private).';\n else if (code === 101 || code === 150) desc = 'The owner of the requested video does not allow it to be played in embedded players.';\n\n if (this._overlay) {\n this._overlay.style.cssText =\n 'position:absolute;top:0;left:0;right:0;bottom:0;padding:24px;' +\n 'background:#151515;color:#f1f1f1;font-size:14px;' +\n 'font-family:Roboto,Arial,sans-serif;display:flex;' +\n 'flex-direction:column;align-items:center;justify-content:center;' +\n 'text-align:center;pointer-events:auto;z-index:10;box-sizing:border-box;';\n this._overlay.innerHTML =\n '<div style=\"font-size:28px;margin-bottom:8px;\">⚠️</div>' +\n '<div style=\"font-weight:bold;margin-bottom:4px;color:#fff;\">An error occurred.</div>' +\n '<div style=\"font-size:12px;color:#aaa;line-height:1.4;max-width:280px;\">' + desc + ' (Error Code: ' + code + ')</div>';\n }\n\n if (this._options.events && typeof this._options.events.onError === 'function') {\n this._options.events.onError({ target: this, data: code });\n }\n };\n\n // Playback controls\n Player.prototype.playVideo = function() {\n if (this._hasError) return;\n this._video.play().catch(function() {});\n };\n Player.prototype.pauseVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n };\n Player.prototype.stopVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n this._video.currentTime = 0;\n };\n Player.prototype.seekTo = function(seconds) {\n if (this._hasError) return;\n this._video.currentTime = seconds;\n };\n\n // Volume controls\n Player.prototype.setVolume = function(vol) {\n this._volume = vol;\n this._video.volume = vol / 100;\n };\n Player.prototype.getVolume = function() { return this._volume; };\n Player.prototype.mute = function() { this._muted = true; this._video.muted = true; };\n Player.prototype.unMute = function() { this._muted = false; this._video.muted = false; };\n Player.prototype.isMuted = function() { return this._muted; };\n\n // State queries\n Player.prototype.getPlayerState = function() { return this._state; };\n Player.prototype.getCurrentTime = function() { return this._video.currentTime; };\n Player.prototype.getDuration = function() { return this._video.duration || 0; };\n Player.prototype.getVideoUrl = function() {\n return 'https://www.youtube.com/watch?v=' + (this._options.videoId || 'test');\n };\n Player.prototype.getVideoData = function() { return this._videoData; };\n\n // Sizing\n Player.prototype.setSize = function(w, h) {\n this._video.width = w;\n this._video.height = h;\n if (this._wrapper) {\n this._wrapper.style.width = w + 'px';\n this._wrapper.style.height = h + 'px';\n }\n };\n\n // Cleanup\n Player.prototype.destroy = function() {\n this._video.pause();\n this._video.src = '';\n this._video.load();\n if (this._container) {\n this._container.innerHTML = '';\n }\n };\n\n // Install globals\n window.YT = {\n Player: Player,\n PlayerState: PlayerState\n };\n\n // Fire the API-ready callback\n if (typeof window.onYouTubeIframeAPIReady === 'function') {\n setTimeout(function() { window.onYouTubeIframeAPIReady(); }, 0);\n }\n})();`;\n\nexport function generatePlayerShim(twinBaseUrl: string): string {\n return SHIM_TEMPLATE.replace(/__TWIN_BASE_URL__/g, twinBaseUrl);\n}\n"],"names":["SHIM_TEMPLATE","generatePlayerShim","twinBaseUrl","replace"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA,OAAO,MAAMA,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkSzB,CAAC,CAAC;AAEP,OAAO,SAASC,mBAAmBC,WAAmB;IACpD,OAAOF,cAAcG,OAAO,CAAC,sBAAsBD;AACrD"}
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/player-shim.ts"],"sourcesContent":["export const SHIM_TEMPLATE = `(function() {\n 'use strict';\n\n var TWIN_BASE = '__TWIN_BASE_URL__';\n\n var PlayerState = {\n UNSTARTED: -1,\n ENDED: 0,\n PLAYING: 1,\n PAUSED: 2,\n BUFFERING: 3,\n CUED: 5\n };\n\n function Player(elementIdOrElement, options) {\n options = options || {};\n\n // Resolve target container\n this._container = typeof elementIdOrElement === 'string'\n ? document.getElementById(elementIdOrElement)\n : elementIdOrElement;\n\n if (!this._container) {\n throw new Error('YT.Player: target element not found: ' + elementIdOrElement);\n }\n\n this._options = options;\n this._state = PlayerState.UNSTARTED;\n this._volume = 100;\n this._muted = false;\n this._hasError = false;\n this._errorCode = null;\n this._videoData = {\n video_id: options.videoId || '',\n title: '',\n author: ''\n };\n\n // Create HTML5 <video> element\n this._video = document.createElement('video');\n this._video.src = TWIN_BASE + '/test-assets/sample.mp4';\n this._video.width = options.width || 640;\n this._video.height = options.height || 360;\n this._video.controls = !!(options.playerVars && options.playerVars.controls !== 0);\n this._video.playsInline = !!(options.playerVars && options.playerVars.playsinline);\n this._video.style.width = '100%';\n this._video.style.height = '100%';\n this._video.style.backgroundColor = '#000';\n this._video.style.display = 'block';\n\n // Override video.play to check error state\n var self = this;\n var originalPlay = this._video.play;\n this._video.play = function() {\n if (self._hasError) {\n return Promise.reject(new Error('Playback blocked due to player error ' + self._errorCode));\n }\n return originalPlay.apply(this, arguments);\n };\n\n // Create metadata overlay\n this._overlay = document.createElement('div');\n this._overlay.style.cssText =\n 'position:absolute;bottom:0;left:0;right:0;padding:8px 12px;' +\n 'background:linear-gradient(transparent, rgba(0,0,0,0.8));' +\n 'color:#fff;font-size:14px;font-family:Roboto,Arial,sans-serif;' +\n 'pointer-events:none;z-index:10;';\n this._loadVideoMetadata(options.videoId);\n\n // Wrap in positioned container\n this._wrapper = document.createElement('div');\n this._wrapper.style.cssText = 'position:relative;overflow:hidden;width:' + (options.width ? options.width + 'px' : '100%') + ';height:' + (options.height ? options.height + 'px' : '100%') + ';';\n this._wrapper.appendChild(this._video);\n this._wrapper.appendChild(this._overlay);\n\n // Replace target element content\n this._container.innerHTML = '';\n this._container.appendChild(this._wrapper);\n\n // Wire HTML5 video events -> YT state changes\n this._video.addEventListener('play', function() {\n if (self._hasError) {\n self._video.pause();\n }\n });\n this._video.addEventListener('playing', function() {\n if (self._hasError) {\n self._video.pause();\n return;\n }\n self._state = PlayerState.PLAYING;\n self._fireStateChange();\n });\n this._video.addEventListener('pause', function() {\n if (self._video.ended) return; // 'ended' fires separately\n if (self._hasError) return;\n self._state = PlayerState.PAUSED;\n self._fireStateChange();\n });\n this._video.addEventListener('ended', function() {\n if (self._hasError) return;\n self._state = PlayerState.ENDED;\n self._fireStateChange();\n });\n this._video.addEventListener('waiting', function() {\n if (self._hasError) return;\n self._state = PlayerState.BUFFERING;\n self._fireStateChange();\n });\n\n // Fire onReady asynchronously (matches real YT behavior)\n setTimeout(function() {\n if (options.events && typeof options.events.onReady === 'function') {\n options.events.onReady({ target: self });\n }\n }, 100);\n\n }\n\n Player.prototype._loadVideoMetadata = function(videoId) {\n if (!videoId) {\n this._overlay.textContent = 'YouTube Twin Player';\n return;\n }\n\n var self = this;\n\n // Check error-mode control endpoint first\n fetch(TWIN_BASE + '/test/youtube/error-mode')\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data.active && data.mode && data.mode.startsWith('player-error-')) {\n var code = parseInt(data.mode.split('-')[2], 10);\n setTimeout(function() {\n self._fireError(code);\n }, 200);\n return;\n }\n\n // Auto-play if configured and no error\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n\n // Fetch actual mock metadata\n fetch(TWIN_BASE + '/youtube/v3/search?q=' + encodeURIComponent(videoId) +\n '&type=video&maxResults=1&key=shim&part=snippet')\n .then(function(res) {\n if (!res.ok) throw new Error('Search failed');\n return res.json();\n })\n .then(function(searchData) {\n if (searchData.items && searchData.items.length > 0) {\n var snippet = searchData.items[0].snippet;\n self._videoData = {\n video_id: videoId,\n title: snippet.title,\n author: snippet.channelTitle\n };\n self._overlay.textContent = snippet.title + ' - ' + snippet.channelTitle;\n } else {\n self._overlay.textContent = 'Video: ' + videoId;\n }\n })\n .catch(function() {\n self._overlay.textContent = 'Video: ' + videoId;\n });\n })\n .catch(function() {\n // Auto-play if configured and endpoint query fails\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n self._overlay.textContent = 'Video: ' + videoId;\n });\n };\n\n Player.prototype._fireStateChange = function() {\n if (this._options.events && typeof this._options.events.onStateChange === 'function') {\n this._options.events.onStateChange({ target: this, data: this._state });\n }\n };\n\n Player.prototype._fireError = function(code) {\n this._hasError = true;\n this._errorCode = code;\n this._state = PlayerState.UNSTARTED;\n\n // Pause and clean up video\n if (this._video) {\n this._video.pause();\n this._video.src = '';\n try {\n this._video.removeAttribute('src');\n this._video.load();\n } catch (e) {}\n }\n\n var desc = 'An unknown player error occurred.';\n if (code === 2) desc = 'The request contains an invalid parameter value.';\n else if (code === 5) desc = 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';\n else if (code === 100) desc = 'The video requested was not found (removed or marked as private).';\n else if (code === 101 || code === 150) desc = 'The owner of the requested video does not allow it to be played in embedded players.';\n\n if (this._overlay) {\n this._overlay.style.cssText =\n 'position:absolute;top:0;left:0;right:0;bottom:0;padding:24px;' +\n 'background:#151515;color:#f1f1f1;font-size:14px;' +\n 'font-family:Roboto,Arial,sans-serif;display:flex;' +\n 'flex-direction:column;align-items:center;justify-content:center;' +\n 'text-align:center;pointer-events:auto;z-index:10;box-sizing:border-box;';\n this._overlay.innerHTML =\n '<div style=\"font-size:28px;margin-bottom:8px;\">⚠️</div>' +\n '<div style=\"font-weight:bold;margin-bottom:4px;color:#fff;\">An error occurred.</div>' +\n '<div style=\"font-size:12px;color:#aaa;line-height:1.4;max-width:280px;\">' + desc + ' (Error Code: ' + code + ')</div>';\n }\n\n if (this._options.events && typeof this._options.events.onError === 'function') {\n this._options.events.onError({ target: this, data: code });\n }\n };\n\n // Playback controls\n Player.prototype.playVideo = function() {\n if (this._hasError) return;\n this._video.play().catch(function() {});\n };\n Player.prototype.pauseVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n };\n Player.prototype.stopVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n this._video.currentTime = 0;\n };\n Player.prototype.seekTo = function(seconds) {\n if (this._hasError) return;\n this._video.currentTime = seconds;\n };\n\n // Volume controls\n Player.prototype.setVolume = function(vol) {\n this._volume = vol;\n this._video.volume = vol / 100;\n };\n Player.prototype.getVolume = function() { return this._volume; };\n Player.prototype.mute = function() { this._muted = true; this._video.muted = true; };\n Player.prototype.unMute = function() { this._muted = false; this._video.muted = false; };\n Player.prototype.isMuted = function() { return this._muted; };\n\n // State queries\n Player.prototype.getPlayerState = function() { return this._state; };\n Player.prototype.getCurrentTime = function() { return this._video.currentTime; };\n Player.prototype.getDuration = function() { return this._video.duration || 0; };\n Player.prototype.getVideoUrl = function() {\n return 'https://www.youtube.com/watch?v=' + (this._options.videoId || 'test');\n };\n Player.prototype.getVideoData = function() { return this._videoData; };\n\n // Sizing\n Player.prototype.setSize = function(w, h) {\n this._video.width = w;\n this._video.height = h;\n if (this._wrapper) {\n this._wrapper.style.width = w + 'px';\n this._wrapper.style.height = h + 'px';\n }\n };\n\n // Cleanup\n Player.prototype.destroy = function() {\n this._video.pause();\n this._video.src = '';\n this._video.load();\n if (this._container) {\n this._container.innerHTML = '';\n }\n };\n\n // Install globals\n window.YT = {\n Player: Player,\n PlayerState: PlayerState\n };\n\n // Fire the API-ready callback\n if (typeof window.onYouTubeIframeAPIReady === 'function') {\n setTimeout(function() { window.onYouTubeIframeAPIReady(); }, 0);\n }\n})();`;\n\nexport function generatePlayerShim(twinBaseUrl: string): string {\n return SHIM_TEMPLATE.replace(/__TWIN_BASE_URL__/g, twinBaseUrl);\n}\n"],"names":["SHIM_TEMPLATE","generatePlayerShim","twinBaseUrl","replace"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAkSzB,CAAC,CAAC;AAEP,OAAO,SAASC,mBAAmBC,WAAmB;IACpD,OAAOF,cAAcG,OAAO,CAAC,sBAAsBD;AACrD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/search-fixtures.ts"],"sourcesContent":["/**\n * Canned YouTube video search fixtures.\n *\n * Each fixture represents a single video result with realistic metadata.\n * The fixture set spans diverse content categories to support query-aware\n * filtering in tests.\n */\n\nexport interface VideoFixture {\n videoId: string;\n title: string;\n channelTitle: string;\n channelId: string;\n description: string;\n publishedAt: string; // ISO 8601\n thumbnailIndex: number; // Maps to thumbnail-XX.jpg\n duration: string; // ISO 8601 duration (e.g., \"PT20M15S\")\n viewCount: string;\n tags: string[];\n}\n\nexport const videoFixtures: VideoFixture[] = [\n {\n videoId: 'test-vid-001',\n title: 'Morning Yoga Flow - 20 Minutes',\n channelTitle: 'Wellness Daily',\n channelId: 'UC-wellness-daily-001',\n description:\n 'Start your day with this gentle 20-minute yoga flow for all levels.',\n publishedAt: '2024-03-15T10:00:00Z',\n thumbnailIndex: 1,\n duration: 'PT20M15S',\n viewCount: '1250000',\n tags: ['yoga', 'morning', 'wellness', 'fitness', 'stretching'],\n },\n {\n videoId: 'test-vid-002',\n title: 'How to Make Classic Italian Pasta carbonara',\n channelTitle: 'Chef Roberto',\n channelId: 'UC-chef-roberto-002',\n description:\n 'Learn the secrets to authentic, creamy Roman carbonara without cream!',\n publishedAt: '2024-03-14T17:30:00Z',\n thumbnailIndex: 2,\n duration: 'PT12M45S',\n viewCount: '450000',\n tags: ['cooking', 'pasta', 'carbonara', 'italian', 'recipe'],\n },\n {\n videoId: 'test-vid-003',\n title: 'TypeScript in 100 Seconds',\n channelTitle: 'Code Academy',\n channelId: 'UC-code-academy-003',\n description:\n 'A quick overview of TypeScript features and type safety benefits.',\n publishedAt: '2024-03-13T12:00:00Z',\n thumbnailIndex: 3,\n duration: 'PT1M40S',\n viewCount: '890000',\n tags: ['programming', 'typescript', 'javascript', 'coding', 'tutorial'],\n },\n {\n videoId: 'test-vid-004',\n title: 'Solo Travel Guide: 5 Days in Tokyo',\n channelTitle: 'Wanderlust Journal',\n channelId: 'UC-wanderlust-journal-004',\n description:\n 'Discover the best sights, food, and neighborhoods in Tokyo as a solo traveler.',\n publishedAt: '2024-03-12T08:15:00Z',\n thumbnailIndex: 4,\n duration: 'PT25M10S',\n viewCount: '620000',\n tags: ['travel', 'tokyo', 'japan', 'guide', 'solo'],\n },\n {\n videoId: 'test-vid-005',\n title: 'Acoustic Guitar Cover of Popular Hits 2024',\n channelTitle: 'Acoustic Sessions',\n channelId: 'UC-acoustic-sessions-005',\n description:\n 'Relaxing instrumental acoustic guitar covers for studying or working.',\n publishedAt: '2024-03-11T20:00:00Z',\n thumbnailIndex: 5,\n duration: 'PT45M00S',\n viewCount: '340000',\n tags: ['music', 'guitar', 'acoustic', 'covers', 'relax'],\n },\n {\n videoId: 'test-vid-006',\n title: 'What Happens Inside a Black Hole?',\n channelTitle: 'Curiosity Lab',\n channelId: 'UC-curiosity-lab-006',\n description: 'Exploring the science of event horizons and singularity.',\n publishedAt: '2024-03-10T14:00:00Z',\n thumbnailIndex: 6,\n duration: 'PT15M30S',\n viewCount: '2100000',\n tags: ['science', 'space', 'blackhole', 'physics', 'astronomy'],\n },\n {\n videoId: 'test-vid-007',\n title: 'Elden Ring Speedrun World Record Attempt',\n channelTitle: 'Pixel Adventures',\n channelId: 'UC-pixel-adventures-007',\n description: 'Running through the Lands Between to beat the clock.',\n publishedAt: '2024-03-09T19:45:00Z',\n thumbnailIndex: 7,\n duration: 'PT58M20S',\n viewCount: '150000',\n tags: ['gaming', 'elden-ring', 'speedrun', 'streamer', 'rpg'],\n },\n {\n videoId: 'test-vid-008',\n title: 'Linear Algebra: Matrices Made Intuitive',\n channelTitle: 'Learn Math Easily',\n channelId: 'UC-learn-math-easily-008',\n description:\n 'Visualizing matrix transformations, eigenvectors, and eigenvalues.',\n publishedAt: '2024-03-08T11:00:00Z',\n thumbnailIndex: 8,\n duration: 'PT30M15S',\n viewCount: '980000',\n tags: ['education', 'math', 'algebra', 'matrices', 'visual'],\n },\n {\n videoId: 'test-vid-009',\n title: 'The Great Migration: Serengeti Wild Life',\n channelTitle: 'Earth Documentaries',\n channelId: 'UC-earth-docs-009',\n description:\n 'Witnessing millions of wildebeests cross the Mara River in Africa.',\n publishedAt: '2024-03-07T06:00:00Z',\n thumbnailIndex: 9,\n duration: 'PT48M50S',\n viewCount: '3200000',\n tags: ['nature', 'wildlife', 'serengeti', 'documentary', 'africa'],\n },\n {\n videoId: 'test-vid-010',\n title: 'Landscape Photography: Mastering the Golden Hour',\n channelTitle: 'Shutter Mastery',\n channelId: 'UC-shutter-mastery-010',\n description:\n 'Tips and tricks for capturing perfect lighting in outdoor landscape shots.',\n publishedAt: '2024-03-06T15:40:00Z',\n thumbnailIndex: 10,\n duration: 'PT18M12S',\n viewCount: '280000',\n tags: ['photography', 'landscape', 'camera', 'golden-hour', 'tutorial'],\n },\n {\n videoId: 'test-vid-011',\n title: 'Design System Architecture: Figma to Code',\n channelTitle: 'Creative UI/UX',\n channelId: 'UC-creative-uiux-011',\n description:\n 'How to structure design tokens and components for seamless engineering hands.',\n publishedAt: '2024-03-05T09:30:00Z',\n thumbnailIndex: 11,\n duration: 'PT22M05S',\n viewCount: '410000',\n tags: ['design', 'ui', 'ux', 'figma', 'architecture'],\n },\n {\n videoId: 'test-vid-012',\n title: 'How We Built a $10M SaaS: Startup Lessons',\n channelTitle: 'Startup Grind',\n channelId: 'UC-startup-grind-012',\n description:\n 'An honest breakdown of product-market fit, pricing, and scaling strategies.',\n publishedAt: '2024-03-04T13:10:00Z',\n thumbnailIndex: 12,\n duration: 'PT16M55S',\n viewCount: '540000',\n tags: ['business', 'saas', 'startup', 'scaling', 'lessons'],\n },\n {\n videoId: 'test-vid-013',\n title: 'Learn French: 100 Common Phrases for Beginners',\n channelTitle: 'Speak French Fluently',\n channelId: 'UC-speak-french-013',\n description:\n 'Essential French phrases for travel, greeting, and simple daily conversation.',\n publishedAt: '2024-03-03T07:00:00Z',\n thumbnailIndex: 13,\n duration: 'PT14M20S',\n viewCount: '730000',\n tags: ['language-learning', 'french', 'phrases', 'beginners', 'vocabulary'],\n },\n {\n videoId: 'test-vid-014',\n title: 'DIY Oak Coffee Table: Woodworking Guide',\n channelTitle: 'Home Woodworking',\n channelId: 'UC-home-woodworking-014',\n description:\n 'Building a modern coffee table from scratch using white oak and mortise joinery.',\n publishedAt: '2024-03-02T16:20:00Z',\n thumbnailIndex: 14,\n duration: 'PT28M45S',\n viewCount: '190000',\n tags: ['diy', 'woodworking', 'table', 'furniture', 'maker'],\n },\n {\n videoId: 'test-vid-015',\n title: '15-Minute Mindfulness Meditation for Anxiety',\n channelTitle: 'Calm Minds',\n channelId: 'UC-calm-minds-015',\n description:\n 'A guided meditation to reduce stress, calm the nervous system, and return to center.',\n publishedAt: '2024-03-01T21:00:00Z',\n thumbnailIndex: 15,\n duration: 'PT15M00S',\n viewCount: '1500000',\n tags: ['meditation', 'mindfulness', 'calm', 'anxiety', 'mentalhealth'],\n },\n {\n videoId: 'test-vid-016',\n title: 'HIIT Workout: Full Body Fat Burner',\n channelTitle: 'Active Training',\n channelId: 'UC-active-training-016',\n description:\n 'High intensity interval training routine. No equipment needed.',\n publishedAt: '2024-02-28T09:00:00Z',\n thumbnailIndex: 16,\n duration: 'PT30M00S',\n viewCount: '2500000',\n tags: ['sports', 'hiit', 'workout', 'cardio', 'fitness'],\n },\n {\n videoId: 'test-vid-017',\n title: 'iPhone 16 Pro Max: Honest Review After 6 Months',\n channelTitle: 'Gadget Sphere',\n channelId: 'UC-gadget-sphere-017',\n description:\n 'Is it still worth buying? Camera, battery life, and durability analysis.',\n publishedAt: '2024-02-27T14:30:00Z',\n thumbnailIndex: 17,\n duration: 'PT13M40S',\n viewCount: '870000',\n tags: ['tech-reviews', 'iphone', 'apple', 'smartphone', 'gadgets'],\n },\n {\n videoId: 'test-vid-018',\n title: 'The Rise and Fall of the Roman Empire',\n channelTitle: 'Chronicles of Old',\n channelId: 'UC-chronicles-old-018',\n description:\n 'Understanding the political and social dynamics that shaped European history.',\n publishedAt: '2024-02-26T10:00:00Z',\n thumbnailIndex: 18,\n duration: 'PT52M15S',\n viewCount: '1850000',\n tags: ['history', 'rome', 'roman-empire', 'documentary', 'history-lessons'],\n },\n {\n videoId: 'test-vid-019',\n title: 'When Programmers Try to Socialize (Sketch)',\n channelTitle: 'Laughter Zone',\n channelId: 'UC-laughter-zone-019',\n description:\n 'A funny comedy sketch about introverted developers attending a house party.',\n publishedAt: '2024-02-25T18:00:00Z',\n thumbnailIndex: 19,\n duration: 'PT4M15S',\n viewCount: '950000',\n tags: ['comedy', 'programming', 'developer-humor', 'skit', 'funny'],\n },\n {\n videoId: 'test-vid-020',\n title: 'Behind the Scenes: Hand-Drawn Animation Loop',\n channelTitle: 'Cartoon Studio',\n channelId: 'UC-cartoon-studio-020',\n description:\n 'Timelapse of a 24-frame classical animation walk cycle on light box.',\n publishedAt: '2024-02-24T12:00:00Z',\n thumbnailIndex: 20,\n duration: 'PT8M50S',\n viewCount: '320000',\n tags: ['animation', 'cartoon', 'art', 'timelapse', 'drawing'],\n },\n];\n\n/**\n * Search fixtures using query-aware filtering.\n *\n * Matches `q` against title, description, and tags (case-insensitive).\n * If no matches found, returns a general fallback subset.\n * Never returns empty unless the fixture set itself is empty.\n */\nexport function searchFixtures(\n query: string,\n maxResults = 10,\n fixtures: VideoFixture[] = videoFixtures\n): VideoFixture[] {\n const clampedMax = Math.min(Math.max(maxResults, 1), 50);\n\n if (fixtures.length === 0) {\n return [];\n }\n\n if (!query || query.trim() === '') {\n return fixtures.slice(0, clampedMax);\n }\n\n const q = query.toLowerCase().trim();\n const matches = fixtures.filter(\n (v) =>\n v.title.toLowerCase().includes(q) ||\n v.tags.some((t) => t.toLowerCase().includes(q)) ||\n v.description.toLowerCase().includes(q)\n );\n\n if (matches.length === 0) {\n // Fallback: return general subset so tests don't get unexpected empty results\n return fixtures.slice(0, clampedMax);\n }\n\n return matches.slice(0, clampedMax);\n}\n"],"names":["videoFixtures","videoId","title","channelTitle","channelId","description","publishedAt","thumbnailIndex","duration","viewCount","tags","searchFixtures","query","maxResults","fixtures","clampedMax","Math","min","max","length","trim","slice","q","toLowerCase","matches","filter","v","includes","some","t"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/search-fixtures.ts"],"sourcesContent":["/**\n * Canned YouTube video search fixtures.\n *\n * Each fixture represents a single video result with realistic metadata.\n * The fixture set spans diverse content categories to support query-aware\n * filtering in tests.\n */\n\nexport interface VideoFixture {\n videoId: string;\n title: string;\n channelTitle: string;\n channelId: string;\n description: string;\n publishedAt: string; // ISO 8601\n thumbnailIndex: number; // Maps to thumbnail-XX.jpg\n duration: string; // ISO 8601 duration (e.g., \"PT20M15S\")\n viewCount: string;\n tags: string[];\n}\n\nexport const videoFixtures: VideoFixture[] = [\n {\n videoId: 'test-vid-001',\n title: 'Morning Yoga Flow - 20 Minutes',\n channelTitle: 'Wellness Daily',\n channelId: 'UC-wellness-daily-001',\n description:\n 'Start your day with this gentle 20-minute yoga flow for all levels.',\n publishedAt: '2024-03-15T10:00:00Z',\n thumbnailIndex: 1,\n duration: 'PT20M15S',\n viewCount: '1250000',\n tags: ['yoga', 'morning', 'wellness', 'fitness', 'stretching'],\n },\n {\n videoId: 'test-vid-002',\n title: 'How to Make Classic Italian Pasta carbonara',\n channelTitle: 'Chef Roberto',\n channelId: 'UC-chef-roberto-002',\n description:\n 'Learn the secrets to authentic, creamy Roman carbonara without cream!',\n publishedAt: '2024-03-14T17:30:00Z',\n thumbnailIndex: 2,\n duration: 'PT12M45S',\n viewCount: '450000',\n tags: ['cooking', 'pasta', 'carbonara', 'italian', 'recipe'],\n },\n {\n videoId: 'test-vid-003',\n title: 'TypeScript in 100 Seconds',\n channelTitle: 'Code Academy',\n channelId: 'UC-code-academy-003',\n description:\n 'A quick overview of TypeScript features and type safety benefits.',\n publishedAt: '2024-03-13T12:00:00Z',\n thumbnailIndex: 3,\n duration: 'PT1M40S',\n viewCount: '890000',\n tags: ['programming', 'typescript', 'javascript', 'coding', 'tutorial'],\n },\n {\n videoId: 'test-vid-004',\n title: 'Solo Travel Guide: 5 Days in Tokyo',\n channelTitle: 'Wanderlust Journal',\n channelId: 'UC-wanderlust-journal-004',\n description:\n 'Discover the best sights, food, and neighborhoods in Tokyo as a solo traveler.',\n publishedAt: '2024-03-12T08:15:00Z',\n thumbnailIndex: 4,\n duration: 'PT25M10S',\n viewCount: '620000',\n tags: ['travel', 'tokyo', 'japan', 'guide', 'solo'],\n },\n {\n videoId: 'test-vid-005',\n title: 'Acoustic Guitar Cover of Popular Hits 2024',\n channelTitle: 'Acoustic Sessions',\n channelId: 'UC-acoustic-sessions-005',\n description:\n 'Relaxing instrumental acoustic guitar covers for studying or working.',\n publishedAt: '2024-03-11T20:00:00Z',\n thumbnailIndex: 5,\n duration: 'PT45M00S',\n viewCount: '340000',\n tags: ['music', 'guitar', 'acoustic', 'covers', 'relax'],\n },\n {\n videoId: 'test-vid-006',\n title: 'What Happens Inside a Black Hole?',\n channelTitle: 'Curiosity Lab',\n channelId: 'UC-curiosity-lab-006',\n description: 'Exploring the science of event horizons and singularity.',\n publishedAt: '2024-03-10T14:00:00Z',\n thumbnailIndex: 6,\n duration: 'PT15M30S',\n viewCount: '2100000',\n tags: ['science', 'space', 'blackhole', 'physics', 'astronomy'],\n },\n {\n videoId: 'test-vid-007',\n title: 'Elden Ring Speedrun World Record Attempt',\n channelTitle: 'Pixel Adventures',\n channelId: 'UC-pixel-adventures-007',\n description: 'Running through the Lands Between to beat the clock.',\n publishedAt: '2024-03-09T19:45:00Z',\n thumbnailIndex: 7,\n duration: 'PT58M20S',\n viewCount: '150000',\n tags: ['gaming', 'elden-ring', 'speedrun', 'streamer', 'rpg'],\n },\n {\n videoId: 'test-vid-008',\n title: 'Linear Algebra: Matrices Made Intuitive',\n channelTitle: 'Learn Math Easily',\n channelId: 'UC-learn-math-easily-008',\n description:\n 'Visualizing matrix transformations, eigenvectors, and eigenvalues.',\n publishedAt: '2024-03-08T11:00:00Z',\n thumbnailIndex: 8,\n duration: 'PT30M15S',\n viewCount: '980000',\n tags: ['education', 'math', 'algebra', 'matrices', 'visual'],\n },\n {\n videoId: 'test-vid-009',\n title: 'The Great Migration: Serengeti Wild Life',\n channelTitle: 'Earth Documentaries',\n channelId: 'UC-earth-docs-009',\n description:\n 'Witnessing millions of wildebeests cross the Mara River in Africa.',\n publishedAt: '2024-03-07T06:00:00Z',\n thumbnailIndex: 9,\n duration: 'PT48M50S',\n viewCount: '3200000',\n tags: ['nature', 'wildlife', 'serengeti', 'documentary', 'africa'],\n },\n {\n videoId: 'test-vid-010',\n title: 'Landscape Photography: Mastering the Golden Hour',\n channelTitle: 'Shutter Mastery',\n channelId: 'UC-shutter-mastery-010',\n description:\n 'Tips and tricks for capturing perfect lighting in outdoor landscape shots.',\n publishedAt: '2024-03-06T15:40:00Z',\n thumbnailIndex: 10,\n duration: 'PT18M12S',\n viewCount: '280000',\n tags: ['photography', 'landscape', 'camera', 'golden-hour', 'tutorial'],\n },\n {\n videoId: 'test-vid-011',\n title: 'Design System Architecture: Figma to Code',\n channelTitle: 'Creative UI/UX',\n channelId: 'UC-creative-uiux-011',\n description:\n 'How to structure design tokens and components for seamless engineering hands.',\n publishedAt: '2024-03-05T09:30:00Z',\n thumbnailIndex: 11,\n duration: 'PT22M05S',\n viewCount: '410000',\n tags: ['design', 'ui', 'ux', 'figma', 'architecture'],\n },\n {\n videoId: 'test-vid-012',\n title: 'How We Built a $10M SaaS: Startup Lessons',\n channelTitle: 'Startup Grind',\n channelId: 'UC-startup-grind-012',\n description:\n 'An honest breakdown of product-market fit, pricing, and scaling strategies.',\n publishedAt: '2024-03-04T13:10:00Z',\n thumbnailIndex: 12,\n duration: 'PT16M55S',\n viewCount: '540000',\n tags: ['business', 'saas', 'startup', 'scaling', 'lessons'],\n },\n {\n videoId: 'test-vid-013',\n title: 'Learn French: 100 Common Phrases for Beginners',\n channelTitle: 'Speak French Fluently',\n channelId: 'UC-speak-french-013',\n description:\n 'Essential French phrases for travel, greeting, and simple daily conversation.',\n publishedAt: '2024-03-03T07:00:00Z',\n thumbnailIndex: 13,\n duration: 'PT14M20S',\n viewCount: '730000',\n tags: ['language-learning', 'french', 'phrases', 'beginners', 'vocabulary'],\n },\n {\n videoId: 'test-vid-014',\n title: 'DIY Oak Coffee Table: Woodworking Guide',\n channelTitle: 'Home Woodworking',\n channelId: 'UC-home-woodworking-014',\n description:\n 'Building a modern coffee table from scratch using white oak and mortise joinery.',\n publishedAt: '2024-03-02T16:20:00Z',\n thumbnailIndex: 14,\n duration: 'PT28M45S',\n viewCount: '190000',\n tags: ['diy', 'woodworking', 'table', 'furniture', 'maker'],\n },\n {\n videoId: 'test-vid-015',\n title: '15-Minute Mindfulness Meditation for Anxiety',\n channelTitle: 'Calm Minds',\n channelId: 'UC-calm-minds-015',\n description:\n 'A guided meditation to reduce stress, calm the nervous system, and return to center.',\n publishedAt: '2024-03-01T21:00:00Z',\n thumbnailIndex: 15,\n duration: 'PT15M00S',\n viewCount: '1500000',\n tags: ['meditation', 'mindfulness', 'calm', 'anxiety', 'mentalhealth'],\n },\n {\n videoId: 'test-vid-016',\n title: 'HIIT Workout: Full Body Fat Burner',\n channelTitle: 'Active Training',\n channelId: 'UC-active-training-016',\n description:\n 'High intensity interval training routine. No equipment needed.',\n publishedAt: '2024-02-28T09:00:00Z',\n thumbnailIndex: 16,\n duration: 'PT30M00S',\n viewCount: '2500000',\n tags: ['sports', 'hiit', 'workout', 'cardio', 'fitness'],\n },\n {\n videoId: 'test-vid-017',\n title: 'iPhone 16 Pro Max: Honest Review After 6 Months',\n channelTitle: 'Gadget Sphere',\n channelId: 'UC-gadget-sphere-017',\n description:\n 'Is it still worth buying? Camera, battery life, and durability analysis.',\n publishedAt: '2024-02-27T14:30:00Z',\n thumbnailIndex: 17,\n duration: 'PT13M40S',\n viewCount: '870000',\n tags: ['tech-reviews', 'iphone', 'apple', 'smartphone', 'gadgets'],\n },\n {\n videoId: 'test-vid-018',\n title: 'The Rise and Fall of the Roman Empire',\n channelTitle: 'Chronicles of Old',\n channelId: 'UC-chronicles-old-018',\n description:\n 'Understanding the political and social dynamics that shaped European history.',\n publishedAt: '2024-02-26T10:00:00Z',\n thumbnailIndex: 18,\n duration: 'PT52M15S',\n viewCount: '1850000',\n tags: ['history', 'rome', 'roman-empire', 'documentary', 'history-lessons'],\n },\n {\n videoId: 'test-vid-019',\n title: 'When Programmers Try to Socialize (Sketch)',\n channelTitle: 'Laughter Zone',\n channelId: 'UC-laughter-zone-019',\n description:\n 'A funny comedy sketch about introverted developers attending a house party.',\n publishedAt: '2024-02-25T18:00:00Z',\n thumbnailIndex: 19,\n duration: 'PT4M15S',\n viewCount: '950000',\n tags: ['comedy', 'programming', 'developer-humor', 'skit', 'funny'],\n },\n {\n videoId: 'test-vid-020',\n title: 'Behind the Scenes: Hand-Drawn Animation Loop',\n channelTitle: 'Cartoon Studio',\n channelId: 'UC-cartoon-studio-020',\n description:\n 'Timelapse of a 24-frame classical animation walk cycle on light box.',\n publishedAt: '2024-02-24T12:00:00Z',\n thumbnailIndex: 20,\n duration: 'PT8M50S',\n viewCount: '320000',\n tags: ['animation', 'cartoon', 'art', 'timelapse', 'drawing'],\n },\n];\n\n/**\n * Search fixtures using query-aware filtering.\n *\n * Matches `q` against title, description, and tags (case-insensitive).\n * If no matches found, returns a general fallback subset.\n * Never returns empty unless the fixture set itself is empty.\n */\nexport function searchFixtures(\n query: string,\n maxResults = 10,\n fixtures: VideoFixture[] = videoFixtures\n): VideoFixture[] {\n const clampedMax = Math.min(Math.max(maxResults, 1), 50);\n\n if (fixtures.length === 0) {\n return [];\n }\n\n if (!query || query.trim() === '') {\n return fixtures.slice(0, clampedMax);\n }\n\n const q = query.toLowerCase().trim();\n const matches = fixtures.filter(\n (v) =>\n v.title.toLowerCase().includes(q) ||\n v.tags.some((t) => t.toLowerCase().includes(q)) ||\n v.description.toLowerCase().includes(q)\n );\n\n if (matches.length === 0) {\n // Fallback: return general subset so tests don't get unexpected empty results\n return fixtures.slice(0, clampedMax);\n }\n\n return matches.slice(0, clampedMax);\n}\n"],"names":["videoFixtures","videoId","title","channelTitle","channelId","description","publishedAt","thumbnailIndex","duration","viewCount","tags","searchFixtures","query","maxResults","fixtures","clampedMax","Math","min","max","length","trim","slice","q","toLowerCase","matches","filter","v","includes","some","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/search-response.ts"],"sourcesContent":["import { VideoFixture } from './search-fixtures.js';\n\n/**\n * Format an array of VideoFixture objects into a youtube#searchListResponse.\n *\n * The response shape exactly matches the Google YouTube Data API v3\n * search.list endpoint documented at:\n * https://developers.google.com/youtube/v3/docs/search/list\n */\nexport function formatSearchResponse(\n fixtures: VideoFixture[],\n baseUrl: string,\n totalResults: number\n) {\n const etagSeed = fixtures.map((f) => f.videoId).join('-');\n return {\n kind: 'youtube#searchListResponse',\n etag: `test-etag-${etagSeed || 'empty'}`,\n regionCode: 'US',\n pageInfo: {\n totalResults,\n resultsPerPage: fixtures.length,\n },\n items: fixtures.map((fixture) => ({\n kind: 'youtube#searchResult',\n etag: `item-etag-${fixture.videoId}`,\n id: {\n kind: 'youtube#video',\n videoId: fixture.videoId,\n },\n snippet: {\n publishedAt: fixture.publishedAt,\n channelId: fixture.channelId,\n title: fixture.title,\n description: fixture.description,\n thumbnails: {\n default: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 120,\n height: 90,\n },\n medium: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 320,\n height: 180,\n },\n high: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 480,\n height: 360,\n },\n },\n channelTitle: fixture.channelTitle,\n liveBroadcastContent: 'none',\n },\n })),\n };\n}\n"],"names":["formatSearchResponse","fixtures","baseUrl","totalResults","etagSeed","map","f","videoId","join","kind","etag","regionCode","pageInfo","resultsPerPage","length","items","fixture","id","snippet","publishedAt","channelId","title","description","thumbnails","default","url","String","thumbnailIndex","padStart","width","height","medium","high","channelTitle","liveBroadcastContent"],"
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/search-response.ts"],"sourcesContent":["import { VideoFixture } from './search-fixtures.js';\n\n/**\n * Format an array of VideoFixture objects into a youtube#searchListResponse.\n *\n * The response shape exactly matches the Google YouTube Data API v3\n * search.list endpoint documented at:\n * https://developers.google.com/youtube/v3/docs/search/list\n */\nexport function formatSearchResponse(\n fixtures: VideoFixture[],\n baseUrl: string,\n totalResults: number\n) {\n const etagSeed = fixtures.map((f) => f.videoId).join('-');\n return {\n kind: 'youtube#searchListResponse',\n etag: `test-etag-${etagSeed || 'empty'}`,\n regionCode: 'US',\n pageInfo: {\n totalResults,\n resultsPerPage: fixtures.length,\n },\n items: fixtures.map((fixture) => ({\n kind: 'youtube#searchResult',\n etag: `item-etag-${fixture.videoId}`,\n id: {\n kind: 'youtube#video',\n videoId: fixture.videoId,\n },\n snippet: {\n publishedAt: fixture.publishedAt,\n channelId: fixture.channelId,\n title: fixture.title,\n description: fixture.description,\n thumbnails: {\n default: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 120,\n height: 90,\n },\n medium: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 320,\n height: 180,\n },\n high: {\n url: `${baseUrl}/test-assets/thumbnails/thumbnail-${String(\n fixture.thumbnailIndex\n ).padStart(2, '0')}.jpg`,\n width: 480,\n height: 360,\n },\n },\n channelTitle: fixture.channelTitle,\n liveBroadcastContent: 'none',\n },\n })),\n };\n}\n"],"names":["formatSearchResponse","fixtures","baseUrl","totalResults","etagSeed","map","f","videoId","join","kind","etag","regionCode","pageInfo","resultsPerPage","length","items","fixture","id","snippet","publishedAt","channelId","title","description","thumbnails","default","url","String","thumbnailIndex","padStart","width","height","medium","high","channelTitle","liveBroadcastContent"],"mappings":"AAEA;;;;;;CAMC,GACD,OAAO,SAASA,qBACdC,QAAwB,EACxBC,OAAe,EACfC,YAAoB;IAEpB,MAAMC,WAAWH,SAASI,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO,EAAEC,IAAI,CAAC;IACrD,OAAO;QACLC,MAAM;QACNC,MAAM,CAAC,UAAU,EAAEN,YAAY,SAAS;QACxCO,YAAY;QACZC,UAAU;YACRT;YACAU,gBAAgBZ,SAASa,MAAM;QACjC;QACAC,OAAOd,SAASI,GAAG,CAAC,CAACW,UAAa,CAAA;gBAChCP,MAAM;gBACNC,MAAM,CAAC,UAAU,EAAEM,QAAQT,OAAO,EAAE;gBACpCU,IAAI;oBACFR,MAAM;oBACNF,SAASS,QAAQT,OAAO;gBAC1B;gBACAW,SAAS;oBACPC,aAAaH,QAAQG,WAAW;oBAChCC,WAAWJ,QAAQI,SAAS;oBAC5BC,OAAOL,QAAQK,KAAK;oBACpBC,aAAaN,QAAQM,WAAW;oBAChCC,YAAY;wBACVC,SAAS;4BACPC,KAAK,GAAGvB,QAAQ,kCAAkC,EAAEwB,OAClDV,QAAQW,cAAc,EACtBC,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;4BACxBC,OAAO;4BACPC,QAAQ;wBACV;wBACAC,QAAQ;4BACNN,KAAK,GAAGvB,QAAQ,kCAAkC,EAAEwB,OAClDV,QAAQW,cAAc,EACtBC,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;4BACxBC,OAAO;4BACPC,QAAQ;wBACV;wBACAE,MAAM;4BACJP,KAAK,GAAGvB,QAAQ,kCAAkC,EAAEwB,OAClDV,QAAQW,cAAc,EACtBC,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;4BACxBC,OAAO;4BACPC,QAAQ;wBACV;oBACF;oBACAG,cAAcjB,QAAQiB,YAAY;oBAClCC,sBAAsB;gBACxB;YACF,CAAA;IACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/youtube-error-mode.ts"],"sourcesContent":["/**\n * YouTube error mode simulation — pure state management.\n *\n * Design follows GcsErrorModeManager conventions:\n * - Single-mode constraint: only one mode active at a time.\n * - setMode() replaces any existing mode.\n * - reset() and clearMode() clear state to prevent leakage between tests.\n */\n\n// ─── Type Definitions ────────────────────────────────────────────────\n\nexport type YoutubeApiErrorModeType =\n | 'daily-limit-exceeded'\n | 'invalid-api-key'\n | 'empty-results';\n\nexport type YoutubePlayerErrorCode = 2 | 5 | 100 | 101 | 150;\n\nexport type YoutubeErrorModeType =\n | YoutubeApiErrorModeType\n | `player-error-${YoutubePlayerErrorCode}`;\n\nexport interface YoutubeErrorModeConfig {\n type: YoutubeErrorModeType;\n}\n\nexport interface YoutubeErrorSimulationResult {\n status: number;\n body: Record<string, unknown>;\n}\n\n// ─── Error Response Factories ─────────────────────────────────────────\n\nfunction youtubeApiError(\n status: number,\n message: string,\n reason: string,\n domain = 'usageLimits'\n): YoutubeErrorSimulationResult {\n return {\n status,\n body: {\n error: {\n code: status,\n message,\n errors: [{ domain, reason, message }],\n },\n },\n };\n}\n\nfunction emptySearchResponse(): YoutubeErrorSimulationResult {\n return {\n status: 200,\n body: {\n kind: 'youtube#searchListResponse',\n etag: 'empty-etag',\n regionCode: 'US',\n pageInfo: { totalResults: 0, resultsPerPage: 0 },\n items: [],\n },\n };\n}\n\n// ─── Manager ──────────────────────────────────────────────────────────\n\nexport class YoutubeErrorModeManager {\n private activeMode: YoutubeErrorModeConfig | null = null;\n\n setMode(config: YoutubeErrorModeConfig): void {\n this.activeMode = config;\n }\n\n clearMode(): void {\n this.activeMode = null;\n }\n\n getMode(): YoutubeErrorModeConfig | null {\n return this.activeMode;\n }\n\n /**\n * Check if the current mode is a player error mode.\n * Returns the numeric error code, or null if not a player error.\n */\n getPlayerErrorCode(): number | null {\n if (!this.activeMode) return null;\n const match = this.activeMode.type.match(/^player-error-(\\d+)$/);\n return match ? parseInt(match[1], 10) : null;\n }\n\n /**\n * Match a search request against the active API error mode.\n * Returns null for pass-through (no error, or player-only mode).\n */\n matchSearchRequest(): YoutubeErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'daily-limit-exceeded':\n return youtubeApiError(\n 403,\n 'Daily Limit Exceeded',\n 'dailyLimitExceeded'\n );\n case 'invalid-api-key':\n return youtubeApiError(400, 'API key not valid', 'keyInvalid');\n case 'empty-results':\n return emptySearchResponse();\n default:\n // Player error modes don't affect the search endpoint\n return null;\n }\n }\n\n reset(): void {\n this.activeMode = null;\n }\n}\n"],"names":["youtubeApiError","status","message","reason","domain","body","error","code","errors","emptySearchResponse","kind","etag","regionCode","pageInfo","totalResults","resultsPerPage","items","YoutubeErrorModeManager","setMode","config","activeMode","clearMode","getMode","getPlayerErrorCode","match","type","parseInt","matchSearchRequest","reset"],"
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/youtube-error-mode.ts"],"sourcesContent":["/**\n * YouTube error mode simulation — pure state management.\n *\n * Design follows GcsErrorModeManager conventions:\n * - Single-mode constraint: only one mode active at a time.\n * - setMode() replaces any existing mode.\n * - reset() and clearMode() clear state to prevent leakage between tests.\n */\n\n// ─── Type Definitions ────────────────────────────────────────────────\n\nexport type YoutubeApiErrorModeType =\n | 'daily-limit-exceeded'\n | 'invalid-api-key'\n | 'empty-results';\n\nexport type YoutubePlayerErrorCode = 2 | 5 | 100 | 101 | 150;\n\nexport type YoutubeErrorModeType =\n | YoutubeApiErrorModeType\n | `player-error-${YoutubePlayerErrorCode}`;\n\nexport interface YoutubeErrorModeConfig {\n type: YoutubeErrorModeType;\n}\n\nexport interface YoutubeErrorSimulationResult {\n status: number;\n body: Record<string, unknown>;\n}\n\n// ─── Error Response Factories ─────────────────────────────────────────\n\nfunction youtubeApiError(\n status: number,\n message: string,\n reason: string,\n domain = 'usageLimits'\n): YoutubeErrorSimulationResult {\n return {\n status,\n body: {\n error: {\n code: status,\n message,\n errors: [{ domain, reason, message }],\n },\n },\n };\n}\n\nfunction emptySearchResponse(): YoutubeErrorSimulationResult {\n return {\n status: 200,\n body: {\n kind: 'youtube#searchListResponse',\n etag: 'empty-etag',\n regionCode: 'US',\n pageInfo: { totalResults: 0, resultsPerPage: 0 },\n items: [],\n },\n };\n}\n\n// ─── Manager ──────────────────────────────────────────────────────────\n\nexport class YoutubeErrorModeManager {\n private activeMode: YoutubeErrorModeConfig | null = null;\n\n setMode(config: YoutubeErrorModeConfig): void {\n this.activeMode = config;\n }\n\n clearMode(): void {\n this.activeMode = null;\n }\n\n getMode(): YoutubeErrorModeConfig | null {\n return this.activeMode;\n }\n\n /**\n * Check if the current mode is a player error mode.\n * Returns the numeric error code, or null if not a player error.\n */\n getPlayerErrorCode(): number | null {\n if (!this.activeMode) return null;\n const match = this.activeMode.type.match(/^player-error-(\\d+)$/);\n return match ? parseInt(match[1], 10) : null;\n }\n\n /**\n * Match a search request against the active API error mode.\n * Returns null for pass-through (no error, or player-only mode).\n */\n matchSearchRequest(): YoutubeErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'daily-limit-exceeded':\n return youtubeApiError(\n 403,\n 'Daily Limit Exceeded',\n 'dailyLimitExceeded'\n );\n case 'invalid-api-key':\n return youtubeApiError(400, 'API key not valid', 'keyInvalid');\n case 'empty-results':\n return emptySearchResponse();\n default:\n // Player error modes don't affect the search endpoint\n return null;\n }\n }\n\n reset(): void {\n this.activeMode = null;\n }\n}\n"],"names":["youtubeApiError","status","message","reason","domain","body","error","code","errors","emptySearchResponse","kind","etag","regionCode","pageInfo","totalResults","resultsPerPage","items","YoutubeErrorModeManager","setMode","config","activeMode","clearMode","getMode","getPlayerErrorCode","match","type","parseInt","matchSearchRequest","reset"],"mappings":"AAAA;;;;;;;CAOC,GAED,wEAAwE;AAsBxE,yEAAyE;AAEzE,SAASA,gBACPC,MAAc,EACdC,OAAe,EACfC,MAAc,EACdC,SAAS,aAAa;IAEtB,OAAO;QACLH;QACAI,MAAM;YACJC,OAAO;gBACLC,MAAMN;gBACNC;gBACAM,QAAQ;oBAAC;wBAAEJ;wBAAQD;wBAAQD;oBAAQ;iBAAE;YACvC;QACF;IACF;AACF;AAEA,SAASO;IACP,OAAO;QACLR,QAAQ;QACRI,MAAM;YACJK,MAAM;YACNC,MAAM;YACNC,YAAY;YACZC,UAAU;gBAAEC,cAAc;gBAAGC,gBAAgB;YAAE;YAC/CC,OAAO,EAAE;QACX;IACF;AACF;AAEA,yEAAyE;AAEzE,OAAO,MAAMC;IAGXC,QAAQC,MAA8B,EAAQ;QAC5C,IAAI,CAACC,UAAU,GAAGD;IACpB;IAEAE,YAAkB;QAChB,IAAI,CAACD,UAAU,GAAG;IACpB;IAEAE,UAAyC;QACvC,OAAO,IAAI,CAACF,UAAU;IACxB;IAEA;;;GAGC,GACDG,qBAAoC;QAClC,IAAI,CAAC,IAAI,CAACH,UAAU,EAAE,OAAO;QAC7B,MAAMI,QAAQ,IAAI,CAACJ,UAAU,CAACK,IAAI,CAACD,KAAK,CAAC;QACzC,OAAOA,QAAQE,SAASF,KAAK,CAAC,EAAE,EAAE,MAAM;IAC1C;IAEA;;;GAGC,GACDG,qBAA0D;QACxD,IAAI,CAAC,IAAI,CAACP,UAAU,EAAE,OAAO;QAE7B,OAAQ,IAAI,CAACA,UAAU,CAACK,IAAI;YAC1B,KAAK;gBACH,OAAOzB,gBACL,KACA,wBACA;YAEJ,KAAK;gBACH,OAAOA,gBAAgB,KAAK,qBAAqB;YACnD,KAAK;gBACH,OAAOS;YACT;gBACE,sDAAsD;gBACtD,OAAO;QACX;IACF;IAEAmB,QAAc;QACZ,IAAI,CAACR,UAAU,GAAG;IACpB;;aAlDQA,aAA4C;;AAmDtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/youtube-twin.config.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport path from 'node:path';\nimport {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from '../shared/constants.js';\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url));\n\n// Resolved relative to dist/youtube/youtube-twin.config.js when packaged\nconst defaultAssetDir = path.resolve(currentDir, '../../test-assets/youtube');\n\nexport interface YoutubeTwinConfig {\n /** Port the YouTube twin Express server listens on */\n port: number;\n /** External URL consumers use to reach the twin */\n externalUrl: string;\n /** Path to the sample video file */\n sampleVideoPath: string;\n /** Path to the thumbnails directory */\n thumbnailDir: string;\n}\n\nexport const defaultYoutubeConfig: YoutubeTwinConfig = {\n port: DEFAULT_PORTS.YOUTUBE,\n externalUrl: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n sampleVideoPath: path.join(defaultAssetDir, 'sample.mp4'),\n thumbnailDir: path.join(defaultAssetDir, 'thumbnails'),\n};\n\nexport function createYoutubeConfig(\n overrides: Partial<YoutubeTwinConfig> = {}\n): YoutubeTwinConfig {\n const env = new ConfigService(nodeEnvAdapter);\n const envPortVal = env.get(ENV_VARS.YOUTUBE_TWIN_PORT);\n const envPort = envPortVal ? Number.parseInt(envPortVal, 10) : undefined;\n\n const baseConfig: YoutubeTwinConfig = {\n ...defaultYoutubeConfig,\n ...(envPort !== undefined && !Number.isNaN(envPort) && { port: envPort }),\n };\n\n const finalConfig: YoutubeTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `YouTube twin port ${finalConfig.port} is outside the reserved range ` +\n `${PORT_RANGE.min}–${PORT_RANGE.max}.`\n );\n }\n\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultYoutubeConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["fileURLToPath","path","ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","currentDir","dirname","url","defaultAssetDir","resolve","defaultYoutubeConfig","port","YOUTUBE","externalUrl","sampleVideoPath","join","thumbnailDir","createYoutubeConfig","overrides","env","envPortVal","get","YOUTUBE_TWIN_PORT","envPort","Number","parseInt","undefined","baseConfig","isNaN","finalConfig","min","max","Error"],"
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/youtube-twin.config.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url';\nimport path from 'node:path';\nimport {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from '../shared/constants.js';\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url));\n\n// Resolved relative to dist/youtube/youtube-twin.config.js when packaged\nconst defaultAssetDir = path.resolve(currentDir, '../../test-assets/youtube');\n\nexport interface YoutubeTwinConfig {\n /** Port the YouTube twin Express server listens on */\n port: number;\n /** External URL consumers use to reach the twin */\n externalUrl: string;\n /** Path to the sample video file */\n sampleVideoPath: string;\n /** Path to the thumbnails directory */\n thumbnailDir: string;\n}\n\nexport const defaultYoutubeConfig: YoutubeTwinConfig = {\n port: DEFAULT_PORTS.YOUTUBE,\n externalUrl: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n sampleVideoPath: path.join(defaultAssetDir, 'sample.mp4'),\n thumbnailDir: path.join(defaultAssetDir, 'thumbnails'),\n};\n\nexport function createYoutubeConfig(\n overrides: Partial<YoutubeTwinConfig> = {}\n): YoutubeTwinConfig {\n const env = new ConfigService(nodeEnvAdapter);\n const envPortVal = env.get(ENV_VARS.YOUTUBE_TWIN_PORT);\n const envPort = envPortVal ? Number.parseInt(envPortVal, 10) : undefined;\n\n const baseConfig: YoutubeTwinConfig = {\n ...defaultYoutubeConfig,\n ...(envPort !== undefined && !Number.isNaN(envPort) && { port: envPort }),\n };\n\n const finalConfig: YoutubeTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `YouTube twin port ${finalConfig.port} is outside the reserved range ` +\n `${PORT_RANGE.min}–${PORT_RANGE.max}.`\n );\n }\n\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultYoutubeConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["fileURLToPath","path","ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","currentDir","dirname","url","defaultAssetDir","resolve","defaultYoutubeConfig","port","YOUTUBE","externalUrl","sampleVideoPath","join","thumbnailDir","createYoutubeConfig","overrides","env","envPortVal","get","YOUTUBE_TWIN_PORT","envPort","Number","parseInt","undefined","baseConfig","isNaN","finalConfig","min","max","Error"],"mappings":";AAAA,SAASA,aAAa,QAAQ,WAAW;AACzC,OAAOC,UAAU,YAAY;AAC7B,SACEC,aAAa,EACbC,cAAc,QACT,4CAA4C;AACnD,SAASC,aAAa,EAAEC,QAAQ,EAAEC,UAAU,QAAQ,yBAAyB;AAE7E,MAAMC,aAAaN,KAAKO,OAAO,CAACR,cAAc,YAAYS,GAAG;AAE7D,yEAAyE;AACzE,MAAMC,kBAAkBT,KAAKU,OAAO,CAACJ,YAAY;AAajD,OAAO,MAAMK,uBAA0C;IACrDC,MAAMT,cAAcU,OAAO;IAC3BC,aAAa,CAAC,iBAAiB,EAAEX,cAAcU,OAAO,EAAE;IACxDE,iBAAiBf,KAAKgB,IAAI,CAACP,iBAAiB;IAC5CQ,cAAcjB,KAAKgB,IAAI,CAACP,iBAAiB;AAC3C,EAAE;AAEF,OAAO,SAASS,oBACdC,YAAwC,CAAC,CAAC;IAE1C,MAAMC,MAAM,IAAInB,cAAcC;IAC9B,MAAMmB,aAAaD,IAAIE,GAAG,CAAClB,SAASmB,iBAAiB;IACrD,MAAMC,UAAUH,aAAaI,OAAOC,QAAQ,CAACL,YAAY,MAAMM;IAE/D,MAAMC,aAAgC,aACjCjB,sBACCa,YAAYG,aAAa,CAACF,OAAOI,KAAK,CAACL,YAAY;QAAEZ,MAAMY;IAAQ;IAGzE,MAAMM,cAAiC,aAClCF,YACAT;IAGL,IAAIW,YAAYlB,IAAI,GAAGP,WAAW0B,GAAG,IAAID,YAAYlB,IAAI,GAAGP,WAAW2B,GAAG,EAAE;QAC1E,MAAM,IAAIC,MACR,CAAC,kBAAkB,EAAEH,YAAYlB,IAAI,CAAC,+BAA+B,CAAC,GACpE,GAAGP,WAAW0B,GAAG,CAAC,CAAC,EAAE1B,WAAW2B,GAAG,CAAC,CAAC,CAAC;IAE5C;IAEA,IACE,CAACb,UAAUL,WAAW,IACtBgB,YAAYhB,WAAW,KAAKH,qBAAqBG,WAAW,EAC5D;QACAgB,YAAYhB,WAAW,GAAG,CAAC,iBAAiB,EAAEgB,YAAYlB,IAAI,EAAE;IAClE;IAEA,OAAOkB;AACT"}
|
|
@@ -44,9 +44,9 @@ export class YoutubeTwin {
|
|
|
44
44
|
});
|
|
45
45
|
// Test control: override fixtures at runtime
|
|
46
46
|
app.put('/test/youtube/fixtures', (req, res)=>{
|
|
47
|
+
var _ref;
|
|
47
48
|
var _req_body;
|
|
48
|
-
|
|
49
|
-
this.currentFixtures = (_req_body_fixtures = (_req_body = req.body) == null ? void 0 : _req_body.fixtures) != null ? _req_body_fixtures : [
|
|
49
|
+
this.currentFixtures = (_ref = (_req_body = req.body) == null ? void 0 : _req_body.fixtures) != null ? _ref : [
|
|
50
50
|
...videoFixtures
|
|
51
51
|
];
|
|
52
52
|
res.json({
|
|
@@ -91,11 +91,11 @@ export class YoutubeTwin {
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
app.get('/test/youtube/error-mode', (_req, res)=>{
|
|
94
|
+
var _ref;
|
|
94
95
|
const mode = this.errorModeManager.getMode();
|
|
95
|
-
var _mode_type;
|
|
96
96
|
return res.json({
|
|
97
97
|
active: mode !== null,
|
|
98
|
-
mode: (
|
|
98
|
+
mode: (_ref = mode == null ? void 0 : mode.type) != null ? _ref : null
|
|
99
99
|
});
|
|
100
100
|
});
|
|
101
101
|
// Serve YouTube IFrame Player API Shim JS
|
|
@@ -141,7 +141,7 @@ export class YoutubeTwin {
|
|
|
141
141
|
});
|
|
142
142
|
await res.text(); // Consume body to release socket
|
|
143
143
|
return res.ok;
|
|
144
|
-
} catch (
|
|
144
|
+
} catch (unused) {
|
|
145
145
|
return false;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/youtube/youtube-twin.ts"],"sourcesContent":["import http from 'node:http';\nimport path from 'node:path';\nimport express, { Request, Response } from 'express';\nimport cors from 'cors';\nimport {\n createYoutubeConfig,\n YoutubeTwinConfig,\n} from './youtube-twin.config.js';\nimport {\n VideoFixture,\n videoFixtures,\n searchFixtures,\n} from './search-fixtures.js';\nimport { formatSearchResponse } from './search-response.js';\nimport { generatePlayerShim } from './player-shim.js';\nimport { YoutubeErrorModeManager } from './youtube-error-mode.js';\n\nexport class YoutubeTwin {\n private readonly config: YoutubeTwinConfig;\n private server: http.Server | null = null;\n private currentFixtures: VideoFixture[] = [];\n private readonly errorModeManager = new YoutubeErrorModeManager();\n\n constructor(overrides?: Partial<YoutubeTwinConfig>) {\n this.config = createYoutubeConfig(overrides);\n this.currentFixtures = [...videoFixtures];\n }\n\n async start(): Promise<void> {\n if (this.server) return; // Already started\n\n const app = express();\n app.use(cors());\n app.use(express.json());\n\n // Serve static test assets (sample video and thumbnails)\n const assetsDir = path.resolve(this.config.thumbnailDir, '..');\n app.use('/test-assets', express.static(assetsDir));\n\n // YouTube Data API — Search\n app.get('/youtube/v3/search', (req: Request, res: Response) => {\n const errorResult = this.errorModeManager.matchSearchRequest();\n if (errorResult) {\n return res.status(errorResult.status).json(errorResult.body);\n }\n\n const {\n q = '',\n maxResults = '10',\n key,\n } = req.query as Record<string, string>;\n\n if (!key) {\n return res.status(400).json({\n error: {\n code: 400,\n message: 'API key not valid. Please pass a valid API key.',\n errors: [\n {\n domain: 'usageLimits',\n reason: 'keyInvalid',\n message: 'API key not valid.',\n },\n ],\n },\n });\n }\n\n const limit = Number.parseInt(maxResults, 10);\n const results = searchFixtures(\n q,\n Number.isNaN(limit) ? 10 : limit,\n this.currentFixtures\n );\n const response = formatSearchResponse(\n results,\n this.config.externalUrl,\n results.length\n );\n\n return res.json(response);\n });\n\n // Test control: override fixtures at runtime\n app.put('/test/youtube/fixtures', (req: Request, res: Response) => {\n this.currentFixtures = req.body?.fixtures ?? [...videoFixtures];\n res.json({ success: true, count: this.currentFixtures.length });\n });\n\n // Health check endpoint\n app.get('/test/youtube/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n // Reset endpoint\n app.post('/test/youtube/reset', async (_req: Request, res: Response) => {\n await this.reset();\n res.json({ success: true });\n });\n\n // Error mode control endpoints\n app.post('/test/youtube/error-mode', (req: Request, res: Response) => {\n const { mode } = req.body;\n if (!mode) {\n return res.status(400).json({ error: 'Missing \"mode\" field' });\n }\n this.errorModeManager.setMode({ type: mode });\n return res.json({ success: true, mode });\n });\n\n app.delete('/test/youtube/error-mode', (_req: Request, res: Response) => {\n this.errorModeManager.clearMode();\n return res.json({ success: true, mode: null });\n });\n\n app.get('/test/youtube/error-mode', (_req: Request, res: Response) => {\n const mode = this.errorModeManager.getMode();\n return res.json({ active: mode !== null, mode: mode?.type ?? null });\n });\n\n // Serve YouTube IFrame Player API Shim JS\n const shimJs = generatePlayerShim(this.config.externalUrl);\n\n app.get('/iframe_api', (_req: Request, res: Response) => {\n res.type('application/javascript').send(shimJs);\n });\n\n app.get('/shim/youtube-player.js', (_req: Request, res: Response) => {\n res.type('application/javascript').send(shimJs);\n });\n\n return new Promise<void>((resolve, reject) => {\n this.server = app.listen(this.config.port, () => {\n console.log(\n `[YoutubeTwin] Express server listening on port ${this.config.port}`\n );\n resolve();\n });\n this.server.on('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const server = this.server;\n if (!server) return;\n return new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) return reject(err);\n this.server = null;\n console.log('[YoutubeTwin] Express server stopped');\n resolve();\n });\n });\n }\n\n async reset(): Promise<void> {\n this.errorModeManager.reset();\n this.currentFixtures = [...videoFixtures];\n }\n\n async isHealthy(): Promise<boolean> {\n try {\n const res = await fetch(\n `${this.config.externalUrl}/test/youtube/health`,\n {\n headers: { Connection: 'close' },\n }\n );\n await res.text(); // Consume body to release socket\n return res.ok;\n } catch {\n return false;\n }\n }\n\n getEmulatorHost(): string {\n return this.config.externalUrl;\n }\n}\n"],"names":["path","express","cors","createYoutubeConfig","videoFixtures","searchFixtures","formatSearchResponse","generatePlayerShim","YoutubeErrorModeManager","YoutubeTwin","start","server","app","use","json","assetsDir","resolve","config","thumbnailDir","static","get","req","res","errorResult","errorModeManager","matchSearchRequest","status","body","q","maxResults","key","query","error","code","message","errors","domain","reason","limit","Number","parseInt","results","isNaN","currentFixtures","response","externalUrl","length","put","fixtures","success","count","_req","post","reset","mode","setMode","type","delete","clearMode","getMode","active","shimJs","send","Promise","reject","listen","port","console","log","on","stop","close","err","isHealthy","fetch","headers","Connection","text","ok","getEmulatorHost","constructor","overrides"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/youtube-twin.ts"],"sourcesContent":["import http from 'node:http';\nimport path from 'node:path';\nimport express, { Request, Response } from 'express';\nimport cors from 'cors';\nimport {\n createYoutubeConfig,\n YoutubeTwinConfig,\n} from './youtube-twin.config.js';\nimport {\n VideoFixture,\n videoFixtures,\n searchFixtures,\n} from './search-fixtures.js';\nimport { formatSearchResponse } from './search-response.js';\nimport { generatePlayerShim } from './player-shim.js';\nimport { YoutubeErrorModeManager } from './youtube-error-mode.js';\n\nexport class YoutubeTwin {\n private readonly config: YoutubeTwinConfig;\n private server: http.Server | null = null;\n private currentFixtures: VideoFixture[] = [];\n private readonly errorModeManager = new YoutubeErrorModeManager();\n\n constructor(overrides?: Partial<YoutubeTwinConfig>) {\n this.config = createYoutubeConfig(overrides);\n this.currentFixtures = [...videoFixtures];\n }\n\n async start(): Promise<void> {\n if (this.server) return; // Already started\n\n const app = express();\n app.use(cors());\n app.use(express.json());\n\n // Serve static test assets (sample video and thumbnails)\n const assetsDir = path.resolve(this.config.thumbnailDir, '..');\n app.use('/test-assets', express.static(assetsDir));\n\n // YouTube Data API — Search\n app.get('/youtube/v3/search', (req: Request, res: Response) => {\n const errorResult = this.errorModeManager.matchSearchRequest();\n if (errorResult) {\n return res.status(errorResult.status).json(errorResult.body);\n }\n\n const {\n q = '',\n maxResults = '10',\n key,\n } = req.query as Record<string, string>;\n\n if (!key) {\n return res.status(400).json({\n error: {\n code: 400,\n message: 'API key not valid. Please pass a valid API key.',\n errors: [\n {\n domain: 'usageLimits',\n reason: 'keyInvalid',\n message: 'API key not valid.',\n },\n ],\n },\n });\n }\n\n const limit = Number.parseInt(maxResults, 10);\n const results = searchFixtures(\n q,\n Number.isNaN(limit) ? 10 : limit,\n this.currentFixtures\n );\n const response = formatSearchResponse(\n results,\n this.config.externalUrl,\n results.length\n );\n\n return res.json(response);\n });\n\n // Test control: override fixtures at runtime\n app.put('/test/youtube/fixtures', (req: Request, res: Response) => {\n this.currentFixtures = req.body?.fixtures ?? [...videoFixtures];\n res.json({ success: true, count: this.currentFixtures.length });\n });\n\n // Health check endpoint\n app.get('/test/youtube/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n // Reset endpoint\n app.post('/test/youtube/reset', async (_req: Request, res: Response) => {\n await this.reset();\n res.json({ success: true });\n });\n\n // Error mode control endpoints\n app.post('/test/youtube/error-mode', (req: Request, res: Response) => {\n const { mode } = req.body;\n if (!mode) {\n return res.status(400).json({ error: 'Missing \"mode\" field' });\n }\n this.errorModeManager.setMode({ type: mode });\n return res.json({ success: true, mode });\n });\n\n app.delete('/test/youtube/error-mode', (_req: Request, res: Response) => {\n this.errorModeManager.clearMode();\n return res.json({ success: true, mode: null });\n });\n\n app.get('/test/youtube/error-mode', (_req: Request, res: Response) => {\n const mode = this.errorModeManager.getMode();\n return res.json({ active: mode !== null, mode: mode?.type ?? null });\n });\n\n // Serve YouTube IFrame Player API Shim JS\n const shimJs = generatePlayerShim(this.config.externalUrl);\n\n app.get('/iframe_api', (_req: Request, res: Response) => {\n res.type('application/javascript').send(shimJs);\n });\n\n app.get('/shim/youtube-player.js', (_req: Request, res: Response) => {\n res.type('application/javascript').send(shimJs);\n });\n\n return new Promise<void>((resolve, reject) => {\n this.server = app.listen(this.config.port, () => {\n console.log(\n `[YoutubeTwin] Express server listening on port ${this.config.port}`\n );\n resolve();\n });\n this.server.on('error', reject);\n });\n }\n\n async stop(): Promise<void> {\n const server = this.server;\n if (!server) return;\n return new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) return reject(err);\n this.server = null;\n console.log('[YoutubeTwin] Express server stopped');\n resolve();\n });\n });\n }\n\n async reset(): Promise<void> {\n this.errorModeManager.reset();\n this.currentFixtures = [...videoFixtures];\n }\n\n async isHealthy(): Promise<boolean> {\n try {\n const res = await fetch(\n `${this.config.externalUrl}/test/youtube/health`,\n {\n headers: { Connection: 'close' },\n }\n );\n await res.text(); // Consume body to release socket\n return res.ok;\n } catch {\n return false;\n }\n }\n\n getEmulatorHost(): string {\n return this.config.externalUrl;\n }\n}\n"],"names":["path","express","cors","createYoutubeConfig","videoFixtures","searchFixtures","formatSearchResponse","generatePlayerShim","YoutubeErrorModeManager","YoutubeTwin","start","server","app","use","json","assetsDir","resolve","config","thumbnailDir","static","get","req","res","errorResult","errorModeManager","matchSearchRequest","status","body","q","maxResults","key","query","error","code","message","errors","domain","reason","limit","Number","parseInt","results","isNaN","currentFixtures","response","externalUrl","length","put","fixtures","success","count","_req","post","reset","mode","setMode","type","delete","clearMode","getMode","active","shimJs","send","Promise","reject","listen","port","console","log","on","stop","close","err","isHealthy","fetch","headers","Connection","text","ok","getEmulatorHost","overrides"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-kingdom/shared-backend-integration-test-doubles",
|
|
3
|
-
"version": "0.0.2-
|
|
3
|
+
"version": "0.0.2-21",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"README.md",
|
|
22
22
|
"dist",
|
|
23
|
+
"audio",
|
|
24
|
+
"artwork",
|
|
23
25
|
"test-assets",
|
|
24
26
|
"!**/*.tsbuildinfo"
|
|
25
27
|
],
|
|
@@ -42,7 +44,19 @@
|
|
|
42
44
|
"main": "libs/shared/backend/integration-test-doubles/src/index.ts",
|
|
43
45
|
"tsConfig": "libs/shared/backend/integration-test-doubles/tsconfig.lib.json",
|
|
44
46
|
"skipTypeCheck": true,
|
|
45
|
-
"stripLeadingPaths": true
|
|
47
|
+
"stripLeadingPaths": true,
|
|
48
|
+
"assets": [
|
|
49
|
+
{
|
|
50
|
+
"input": "libs/shared/backend/integration-test-doubles/src/apple-music/audio",
|
|
51
|
+
"glob": "**/*.mp3",
|
|
52
|
+
"output": "audio"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"input": "libs/shared/backend/integration-test-doubles/src/apple-music/artwork",
|
|
56
|
+
"glob": "**/*.svg",
|
|
57
|
+
"output": "artwork"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
46
60
|
}
|
|
47
61
|
},
|
|
48
62
|
"start:twin": {
|
|
@@ -56,11 +70,17 @@
|
|
|
56
70
|
"dependsOn": [
|
|
57
71
|
"^build"
|
|
58
72
|
]
|
|
73
|
+
},
|
|
74
|
+
"start:apple-music-twin": {
|
|
75
|
+
"command": "npx tsx libs/shared/backend/integration-test-doubles/scripts/start-apple-music-twin.ts",
|
|
76
|
+
"dependsOn": [
|
|
77
|
+
"^build"
|
|
78
|
+
]
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
81
|
},
|
|
62
82
|
"dependencies": {
|
|
63
|
-
"@open-kingdom/shared-poly-util-env-config": "0.0.2-
|
|
83
|
+
"@open-kingdom/shared-poly-util-env-config": "0.0.2-21",
|
|
64
84
|
"cors": "^2.8.5",
|
|
65
85
|
"dockerode": "^4.0.6",
|
|
66
86
|
"express": "^5.1.0",
|