From a2567802bcd942c162a60beabb6a60e880085106 Mon Sep 17 00:00:00 2001 From: akashdhake <22520461+akashdhake@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:35:56 +0530 Subject: [PATCH 1/2] feat: add template generate lightning-out command (Option A) Reads a JSON definition file and scaffolds the LO 2.0 metadata via @salesforce/templates. Always warns that the generated IframeWhiteListUrlSettings REPLACES the org's Trusted Domains list on deploy. Generate-only. --- messages/lightningOut.md | 51 ++++++ .../template/generate/lightning-out/index.ts | 92 ++++++++++ .../generate/lightning-out/index.nut.ts | 165 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 messages/lightningOut.md create mode 100644 src/commands/template/generate/lightning-out/index.ts create mode 100644 test/commands/template/generate/lightning-out/index.nut.ts diff --git a/messages/lightningOut.md b/messages/lightningOut.md new file mode 100644 index 00000000..9fafa989 --- /dev/null +++ b/messages/lightningOut.md @@ -0,0 +1,51 @@ +# examples + +- Generate a Lightning Out 2.0 scaffold from a definition file into the current directory: + + <%= config.bin %> <%= command.id %> --definition-file lo-def.json + +- Generate into a specific directory: + + <%= config.bin %> <%= command.id %> --definition-file lo-def.json --output-dir force-app/main/default + +- Overwrite files from a previous run: + + <%= config.bin %> <%= command.id %> --definition-file lo-def.json --force + +# summary + +Generate the metadata scaffold for a Lightning Out 2.0 application. + +# description + +Generates the seven metadata artifact types a Lightning Out 2.0 app requires: LightningOutApp, IframeWhiteListUrlSettings, MyDomain and Security settings, one CorsWhitelistOrigin per host domain, and the External Client Application OAuth trio (ExternalClientApplication, ExtlClntAppGlobalOauthSettings, ExtlClntAppOauthSettings). The command is generate-only; it does not deploy. + +IMPORTANT: Deploying the generated IframeWhiteListUrlSettings REPLACES your org's entire "Trusted Domains for Inline Frames" list (Setup > Security > Session Settings), across every IFrame Type. + +# flags.definition-file.summary + +Path to a JSON file describing the Lightning Out 2.0 app. + +# flags.definition-file.description + +The JSON must contain: name (a valid Metadata API name), runtime (LWR_CORE or CLWR), components (a non-empty array of Lightning web component names), hostDomains (a non-empty array of https origins), and eca (at least a contactEmail; optionally distributionState, callbackUrl, and oauthScopes). + +# flags.force.summary + +Overwrite existing files instead of erroring. + +# flags.force.description + +By default, generation fails if any target file already exists, so a re-run never silently overwrites your edits — notably the REPLACE-type IframeWhiteListUrlSettings file. Pass --force to overwrite. + +# warning.iframe-replace + +The generated IframeWhiteListUrlSettings lists only this app's host domains. Deploying it REPLACES your org's entire "Trusted Domains for Inline Frames" list across all IFrame Types. To preserve existing entries, re-run with --merge-iframe --target-org . + +# error.definition-file-read + +Unable to read definition file %s: %s + +# error.definition-file-json + +Definition file %s is not valid JSON: %s diff --git a/src/commands/template/generate/lightning-out/index.ts b/src/commands/template/generate/lightning-out/index.ts new file mode 100644 index 00000000..ab2b3307 --- /dev/null +++ b/src/commands/template/generate/lightning-out/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { readFile } from 'node:fs/promises'; +import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand, Ux } from '@salesforce/sf-plugins-core'; +import { CreateOutput, LightningOutOptions, TemplateType } from '@salesforce/templates'; +import { Messages, SfError } from '@salesforce/core'; +import { getCustomTemplates, runGenerator } from '../../../../utils/templateCommand.js'; +import { outputDirFlagLightning } from '../../../../utils/flags.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-templates', 'lightningOut'); + +/** Shape of the --definition-file JSON (spec §3.2). */ +type LightningOutDefinition = { + name?: string; + runtime?: LightningOutOptions['runtime']; + components?: string[]; + hostDomains?: string[]; + eca?: LightningOutOptions['eca']; +}; + +/** Parse the definition file as JSON, surfacing a clear error on malformed input. */ +async function readDefinition(file: string): Promise { + let raw: string; + try { + raw = await readFile(file, 'utf8'); + } catch (e) { + throw new SfError(messages.getMessage('error.definition-file-read', [file, (e as Error).message])); + } + try { + return JSON.parse(raw) as LightningOutDefinition; + } catch (e) { + throw new SfError(messages.getMessage('error.definition-file-json', [file, (e as Error).message])); + } +} + +export default class LightningOut extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly state = 'beta'; + public static readonly hidden = true; + + public static readonly flags = { + 'definition-file': Flags.file({ + char: 'f', + summary: messages.getMessage('flags.definition-file.summary'), + description: messages.getMessage('flags.definition-file.description'), + required: true, + exists: true, + }), + 'output-dir': outputDirFlagLightning, + force: Flags.boolean({ + summary: messages.getMessage('flags.force.summary'), + description: messages.getMessage('flags.force.description'), + default: false, + }), + 'api-version': orgApiVersionFlagWithDeprecations, + loglevel, + }; + + public async run(): Promise { + const { flags } = await this.parse(LightningOut); + + const def = await readDefinition(flags['definition-file']); + + this.warn(messages.getMessage('warning.iframe-replace')); + + const flagsAsOptions: LightningOutOptions = { + name: def.name as string, + runtime: def.runtime as LightningOutOptions['runtime'], + components: def.components as string[], + hostDomains: def.hostDomains as string[], + eca: def.eca as LightningOutOptions['eca'], + outputdir: flags['output-dir'], + apiversion: flags['api-version'], + force: flags.force, + }; + + return runGenerator({ + templateType: TemplateType.LightningOut, + opts: flagsAsOptions, + ux: new Ux({ jsonEnabled: this.jsonEnabled() }), + templates: getCustomTemplates(this.configAggregator), + }); + } +} diff --git a/test/commands/template/generate/lightning-out/index.nut.ts b/test/commands/template/generate/lightning-out/index.nut.ts new file mode 100644 index 00000000..1c5bc5cc --- /dev/null +++ b/test/commands/template/generate/lightning-out/index.nut.ts @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import path from 'node:path'; +import fs from 'node:fs'; +import { expect, config } from 'chai'; +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import assert from 'yeoman-assert'; + +config.truncateThreshold = 0; + +describe('template generate lightning-out:', () => { + let session: TestSession; + let defFile: string; + + before(async () => { + session = await TestSession.create({ + project: {}, + devhubAuthStrategy: 'NONE', + }); + defFile = path.join(session.project.dir, 'lo-def.json'); + fs.writeFileSync( + defFile, + JSON.stringify({ + name: 'MyLoApp', + runtime: 'LWR_CORE', + components: ['c-my-button', 'c-my-card'], + hostDomains: ['https://app.example.com', 'https://portal.example.com'], + eca: { contactEmail: 'dev@example.com', distributionState: 'Local', oauthScopes: ['Web', 'Api'] }, + }) + ); + }); + after(async () => { + await session?.clean(); + }); + + const outDir = (name: string): string => path.join(session.project.dir, name); + + const allArtifacts = (dir: string): string[] => [ + path.join(dir, 'lightningOutApps', 'MyLoApp.lightningOutApp-meta.xml'), + path.join(dir, 'iframeWhiteListUrlSettings', 'IframeWhiteListUrlSettings.iframeWhiteListUrlSettings-meta.xml'), + path.join(dir, 'settings', 'MyDomain.settings-meta.xml'), + path.join(dir, 'settings', 'Security.settings-meta.xml'), + path.join(dir, 'corsWhitelistOrigins', 'app_example_com.corsWhitelistOrigin-meta.xml'), + path.join(dir, 'corsWhitelistOrigins', 'portal_example_com.corsWhitelistOrigin-meta.xml'), + path.join(dir, 'externalClientApps', 'MyLoApp.eca-meta.xml'), + path.join(dir, 'extlClntAppGlobalOauthSets', 'MyLoApp.ecaGlblOauth-meta.xml'), + path.join(dir, 'extlClntAppOauthSettings', 'MyLoApp.ecaOauth-meta.xml'), + ]; + + describe('generation', () => { + it('should scaffold all nine metadata artifacts', () => { + const dir = outDir('gen-all'); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + assert.file(allArtifacts(dir)); + }); + + it('should render app name, runtime, and components into the LightningOutApp', () => { + const dir = outDir('gen-app'); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + const app = path.join(dir, 'lightningOutApps', 'MyLoApp.lightningOutApp-meta.xml'); + assert.fileContent(app, 'MyLoApp'); + assert.fileContent(app, 'LWR_CORE'); + assert.fileContent(app, 'c-my-button'); + assert.fileContent(app, 'c-my-card'); + }); + + it('should list this app host domains under LightningOut context in the iframe artifact', () => { + const dir = outDir('gen-iframe'); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + const iframe = path.join( + dir, + 'iframeWhiteListUrlSettings', + 'IframeWhiteListUrlSettings.iframeWhiteListUrlSettings-meta.xml' + ); + assert.fileContent(iframe, 'https://app.example.com'); + assert.fileContent(iframe, 'https://portal.example.com'); + assert.fileContent(iframe, 'LightningOut'); + }); + + it('should warn about the REPLACE risk when not merging', () => { + const dir = outDir('gen-warn'); + const result = execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + expect(result.shellOutput.stderr).to.match(/REPLACES your org's entire/i); + }); + }); + + describe('Option A — no silent overwrite', () => { + it('should fail on a second run without --force', () => { + const dir = outDir('gen-guard'); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + const stderr = execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 'nonZero', + }).shellOutput.stderr; + expect(stderr).to.match(/already exist/i); + }); + + it('should overwrite on a second run with --force', () => { + const dir = outDir('gen-force'); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir}`, { + ensureExitCode: 0, + }); + execCmd(`template generate lightning-out --definition-file ${defFile} --output-dir ${dir} --force`, { + ensureExitCode: 0, + }); + assert.file(allArtifacts(dir)); + }); + }); + + describe('failures', () => { + it('should error when --definition-file is missing', () => { + const stderr = execCmd('template generate lightning-out').shellOutput.stderr; + expect(stderr).to.contain('Missing required flag'); + }); + + it('should error when --definition-file does not exist', () => { + const stderr = execCmd( + `template generate lightning-out --definition-file ${path.join(session.project.dir, 'nope.json')}` + ).shellOutput.stderr; + expect(stderr).to.match(/No file found|does not exist|cannot find/i); + }); + + it('should error on an invalid definition (bad runtime)', () => { + const bad = path.join(session.project.dir, 'bad-runtime.json'); + fs.writeFileSync( + bad, + JSON.stringify({ + name: 'BadApp', + runtime: 'NOPE', + components: ['c-x'], + hostDomains: ['https://app.example.com'], + eca: { contactEmail: 'dev@example.com' }, + }) + ); + const stderr = execCmd( + `template generate lightning-out --definition-file ${bad} --output-dir ${outDir('bad-runtime')}`, + { ensureExitCode: 'nonZero' } + ).shellOutput.stderr; + expect(stderr).to.match(/runtime/i); + }); + + it('should error on malformed JSON', () => { + const bad = path.join(session.project.dir, 'bad-json.json'); + fs.writeFileSync(bad, '{ not valid json '); + const stderr = execCmd( + `template generate lightning-out --definition-file ${bad} --output-dir ${outDir('bad-json')}`, + { ensureExitCode: 'nonZero' } + ).shellOutput.stderr; + expect(stderr).to.match(/not valid JSON/i); + }); + }); +}); From daf1e89b06dd7d10a4c313126c9348a6d4b8fe61 Mon Sep 17 00:00:00 2001 From: akashdhake <22520461+akashdhake@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:35:57 +0530 Subject: [PATCH 2/2] feat: add --merge-iframe (Option B) to lightning-out command Adds --merge-iframe/--target-org and retrieveIframeEntries so the command preserves the org's existing Trusted Domains for Inline Frames across the REPLACE-type deploy. Stacked on the Option A base. --- messages/lightningOut.md | 26 ++++++- .../template/generate/lightning-out/index.ts | 26 ++++++- src/utils/lightningOutIframe.ts | 59 ++++++++++++++++ .../generate/lightning-out/index.nut.ts | 10 +++ test/utils/lightningOutIframe.test.ts | 70 +++++++++++++++++++ 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 src/utils/lightningOutIframe.ts create mode 100644 test/utils/lightningOutIframe.test.ts diff --git a/messages/lightningOut.md b/messages/lightningOut.md index 9fafa989..ab35f28d 100644 --- a/messages/lightningOut.md +++ b/messages/lightningOut.md @@ -12,6 +12,10 @@ <%= config.bin %> <%= command.id %> --definition-file lo-def.json --force +- Preserve the org's existing Trusted Domains for Inline Frames (retrieve-merge) instead of replacing them: + + <%= config.bin %> <%= command.id %> --definition-file lo-def.json --merge-iframe --target-org myOrg + # summary Generate the metadata scaffold for a Lightning Out 2.0 application. @@ -20,7 +24,7 @@ Generate the metadata scaffold for a Lightning Out 2.0 application. Generates the seven metadata artifact types a Lightning Out 2.0 app requires: LightningOutApp, IframeWhiteListUrlSettings, MyDomain and Security settings, one CorsWhitelistOrigin per host domain, and the External Client Application OAuth trio (ExternalClientApplication, ExtlClntAppGlobalOauthSettings, ExtlClntAppOauthSettings). The command is generate-only; it does not deploy. -IMPORTANT: Deploying the generated IframeWhiteListUrlSettings REPLACES your org's entire "Trusted Domains for Inline Frames" list (Setup > Security > Session Settings), across every IFrame Type. +IMPORTANT: Deploying the generated IframeWhiteListUrlSettings REPLACES your org's entire "Trusted Domains for Inline Frames" list (Setup > Security > Session Settings), across every IFrame Type. By default the generated file contains only this app's host domains. To preserve the org's existing entries, pass --merge-iframe with --target-org; the command then retrieves the current list and merges this app's domains into it. # flags.definition-file.summary @@ -38,10 +42,30 @@ Overwrite existing files instead of erroring. By default, generation fails if any target file already exists, so a re-run never silently overwrites your edits — notably the REPLACE-type IframeWhiteListUrlSettings file. Pass --force to overwrite. +# flags.merge-iframe.summary + +Preserve the org's existing Trusted Domains for Inline Frames by merging into them. + +# flags.merge-iframe.description + +Retrieves the target org's current IframeWhiteListUrlSettings and re-emits every existing entry (across all IFrame Types) into the generated file, then adds this app's host domains. Requires --target-org. Without this flag, the generated file contains only this app's host domains and deploying it REPLACES the org's entire list. + +# flags.target-org.summary + +Org whose Trusted Domains for Inline Frames list is retrieved when --merge-iframe is set. + # warning.iframe-replace The generated IframeWhiteListUrlSettings lists only this app's host domains. Deploying it REPLACES your org's entire "Trusted Domains for Inline Frames" list across all IFrame Types. To preserve existing entries, re-run with --merge-iframe --target-org . +# info.merged-iframe-count + +Retrieved %s existing Trusted Domains for Inline Frames entries from the org; the generated file merges this app's host domains into them. + +# error.merge-iframe-requires-org + +--merge-iframe requires a target org. Pass --target-org . + # error.definition-file-read Unable to read definition file %s: %s diff --git a/src/commands/template/generate/lightning-out/index.ts b/src/commands/template/generate/lightning-out/index.ts index ab2b3307..1c6bfaca 100644 --- a/src/commands/template/generate/lightning-out/index.ts +++ b/src/commands/template/generate/lightning-out/index.ts @@ -7,10 +7,11 @@ import { readFile } from 'node:fs/promises'; import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand, Ux } from '@salesforce/sf-plugins-core'; -import { CreateOutput, LightningOutOptions, TemplateType } from '@salesforce/templates'; +import { CreateOutput, IframeWhiteListEntry, LightningOutOptions, TemplateType } from '@salesforce/templates'; import { Messages, SfError } from '@salesforce/core'; import { getCustomTemplates, runGenerator } from '../../../../utils/templateCommand.js'; import { outputDirFlagLightning } from '../../../../utils/flags.js'; +import { retrieveIframeEntries } from '../../../../utils/lightningOutIframe.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-templates', 'lightningOut'); @@ -60,6 +61,14 @@ export default class LightningOut extends SfCommand { description: messages.getMessage('flags.force.description'), default: false, }), + 'merge-iframe': Flags.boolean({ + summary: messages.getMessage('flags.merge-iframe.summary'), + description: messages.getMessage('flags.merge-iframe.description'), + default: false, + }), + 'target-org': Flags.optionalOrg({ + summary: messages.getMessage('flags.target-org.summary'), + }), 'api-version': orgApiVersionFlagWithDeprecations, loglevel, }; @@ -69,7 +78,19 @@ export default class LightningOut extends SfCommand { const def = await readDefinition(flags['definition-file']); - this.warn(messages.getMessage('warning.iframe-replace')); + // Option B (retrieve-merge): read the org's current Trusted Domains for + // Inline Frames so the generator preserves them instead of wiping the list. + let existingIframeEntries: IframeWhiteListEntry[] | undefined; + if (flags['merge-iframe']) { + const org = flags['target-org']; + if (!org) { + throw new SfError(messages.getMessage('error.merge-iframe-requires-org')); + } + existingIframeEntries = await retrieveIframeEntries(org.getConnection(flags['api-version'])); + this.info(messages.getMessage('info.merged-iframe-count', [existingIframeEntries.length])); + } else { + this.warn(messages.getMessage('warning.iframe-replace')); + } const flagsAsOptions: LightningOutOptions = { name: def.name as string, @@ -80,6 +101,7 @@ export default class LightningOut extends SfCommand { outputdir: flags['output-dir'], apiversion: flags['api-version'], force: flags.force, + existingIframeEntries, }; return runGenerator({ diff --git a/src/utils/lightningOutIframe.ts b/src/utils/lightningOutIframe.ts new file mode 100644 index 00000000..4c2190fe --- /dev/null +++ b/src/utils/lightningOutIframe.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2026, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Connection } from '@salesforce/core'; +import { IframeWhiteListEntry } from '@salesforce/templates'; + +/** Metadata API full name of the singleton IframeWhiteListUrlSettings record. */ +const IFRAME_SETTINGS_TYPE = 'IframeWhiteListUrlSettings'; + +/** Shape of one element as returned by the Metadata API read(). */ +type RawIframeUrl = { + url?: string; + context?: string; +}; + +/** Shape of the IframeWhiteListUrlSettings metadata record. */ +type RawIframeSettings = { + fullName?: string; + iframeWhiteListUrls?: RawIframeUrl | RawIframeUrl[]; +}; + +/** + * Normalize the Metadata API's read() result — which returns a single object + * for a scalar field and an array for a repeated field — into a plain array. + */ +function toArray(value: T | T[] | undefined): T[] { + if (value === undefined || value === null) { + return []; + } + return Array.isArray(value) ? value : [value]; +} + +/** + * Option B (retrieve-merge). Read the org's CURRENT "Trusted Domains for Inline + * Frames" list (IframeWhiteListUrlSettings) and return every entry as a + * {url, context} pair — across ALL IFrame Types, not just LightningOut — so the + * generator can re-emit them verbatim and avoid wiping the org's list when the + * REPLACE-type settings artifact is deployed. + */ +export async function retrieveIframeEntries(conn: Connection): Promise { + // `metadata.read` types the metadata type as a closed union that predates + // IframeWhiteListUrlSettings, so cast through the generic string overload. + const read = await conn.metadata.read( + IFRAME_SETTINGS_TYPE as Parameters[0], + IFRAME_SETTINGS_TYPE + ); + const record = (Array.isArray(read) ? read[0] : read) as RawIframeSettings | undefined; + + return toArray(record?.iframeWhiteListUrls) + .filter((u): u is RawIframeUrl & { url: string } => typeof u?.url === 'string' && u.url.length > 0) + .map((u) => ({ + url: u.url, + context: typeof u.context === 'string' && u.context.length > 0 ? u.context : 'LightningOut', + })); +} diff --git a/test/commands/template/generate/lightning-out/index.nut.ts b/test/commands/template/generate/lightning-out/index.nut.ts index 1c5bc5cc..8999f984 100644 --- a/test/commands/template/generate/lightning-out/index.nut.ts +++ b/test/commands/template/generate/lightning-out/index.nut.ts @@ -161,5 +161,15 @@ describe('template generate lightning-out:', () => { ).shellOutput.stderr; expect(stderr).to.match(/not valid JSON/i); }); + + it('should error when --merge-iframe is set without --target-org', () => { + const stderr = execCmd( + `template generate lightning-out --definition-file ${defFile} --output-dir ${outDir( + 'merge-noorg' + )} --merge-iframe`, + { ensureExitCode: 'nonZero' } + ).shellOutput.stderr; + expect(stderr).to.match(/requires a target org|target-org/i); + }); }); }); diff --git a/test/utils/lightningOutIframe.test.ts b/test/utils/lightningOutIframe.test.ts new file mode 100644 index 00000000..80f2d52c --- /dev/null +++ b/test/utils/lightningOutIframe.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { expect } from 'chai'; +import { Connection } from '@salesforce/core'; +import { retrieveIframeEntries } from '../../src/utils/lightningOutIframe.js'; + +/** Build a fake Connection whose metadata.read returns `readResult`. */ +function fakeConn(readResult: unknown): Connection { + return { + metadata: { + read: () => Promise.resolve(readResult), + }, + } as unknown as Connection; +} + +describe('retrieveIframeEntries', () => { + it('returns [] when the settings record has no entries', async () => { + const entries = await retrieveIframeEntries(fakeConn({ fullName: 'IframeWhiteListUrlSettings' })); + expect(entries).to.deep.equal([]); + }); + + it('normalizes a single (scalar) entry into a one-element array', async () => { + const entries = await retrieveIframeEntries( + fakeConn({ iframeWhiteListUrls: { url: 'https://vf.example.com', context: 'Visualforce' } }) + ); + expect(entries).to.deep.equal([{ url: 'https://vf.example.com', context: 'Visualforce' }]); + }); + + it('preserves the context of every entry across IFrame Types', async () => { + const entries = await retrieveIframeEntries( + fakeConn({ + iframeWhiteListUrls: [ + { url: 'https://vf.example.com', context: 'Visualforce' }, + { url: 'https://survey.example.com', context: 'Survey' }, + { url: 'https://lo.example.com', context: 'LightningOut' }, + ], + }) + ); + expect(entries).to.deep.equal([ + { url: 'https://vf.example.com', context: 'Visualforce' }, + { url: 'https://survey.example.com', context: 'Survey' }, + { url: 'https://lo.example.com', context: 'LightningOut' }, + ]); + }); + + it('defaults a missing context to LightningOut', async () => { + const entries = await retrieveIframeEntries(fakeConn({ iframeWhiteListUrls: { url: 'https://x.example.com' } })); + expect(entries).to.deep.equal([{ url: 'https://x.example.com', context: 'LightningOut' }]); + }); + + it('drops entries with no url', async () => { + const entries = await retrieveIframeEntries( + fakeConn({ + iframeWhiteListUrls: [{ url: 'https://ok.example.com', context: 'LightningOut' }, { context: 'Visualforce' }], + }) + ); + expect(entries).to.deep.equal([{ url: 'https://ok.example.com', context: 'LightningOut' }]); + }); + + it('handles read() returning an array of records', async () => { + const entries = await retrieveIframeEntries( + fakeConn([{ iframeWhiteListUrls: { url: 'https://a.example.com', context: 'LightningOut' } }]) + ); + expect(entries).to.deep.equal([{ url: 'https://a.example.com', context: 'LightningOut' }]); + }); +});