-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
802 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Porting } from '../types' | ||
|
||
export type CustomizableEmbedProps = { | ||
// TODO: add styling options | ||
styleConfig?: { | ||
foo?: string | ||
} | ||
} | ||
|
||
type CoreEmbedProps = { | ||
token: string | ||
initialPorting: Porting | ||
} | ||
|
||
type PortingEmbedProps = CoreEmbedProps & CustomizableEmbedProps | ||
|
||
export function PortingEmbed({ token: _, initialPorting }: PortingEmbedProps) { | ||
return <div className="__gigsPortingEmbed">Hello {initialPorting.id}!</div> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { WelcomeEmbed } from '../WelcomeEmbed' | ||
Check failure on line 1 in lib/PortingEmbed/__stories__/WelcomeEmbed.stories.tsx GitHub Actions / lint
|
||
|
||
export default { | ||
title: 'Example/WelcomeEmbed', | ||
component: WelcomeEmbed, | ||
tags: ['autodocs'], | ||
argTypes: { | ||
token: { control: 'text' }, | ||
name: { control: 'text' }, | ||
onCounterChange: { action: 'counterChange' }, | ||
}, | ||
} | ||
|
||
export const Primary = { | ||
args: { | ||
token: 'abc:123', | ||
name: 'Jerry', | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { render, screen } from '@testing-library/preact' | ||
|
||
import { portingFactory } from '@/testing/factories/porting' | ||
|
||
import { PortingEmbed } from '../PortingEmbed' | ||
|
||
it('gets the porting', () => { | ||
const porting = portingFactory.params({ id: 'prt_123' }).build() | ||
render(<PortingEmbed initialPorting={porting} token="abc:123" />) | ||
const greeting = screen.getByText(/Hello prt_123/i) | ||
expect(greeting).toBeInTheDocument() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import { render } from '@testing-library/preact' | ||
|
||
import { connectSessionFactory } from '@/testing/factories/connectSession' | ||
import { portingFactory } from '@/testing/factories/porting' | ||
import { subscriptionFactory } from '@/testing/factories/subscription' | ||
|
||
import { PortingStatus } from '../../types' | ||
import { PortingEmbed } from '../' | ||
|
||
const project = 'test_project' | ||
|
||
async function createFixtures() { | ||
const subscription = await subscriptionFactory.create() | ||
const connectSession = connectSessionFactory | ||
.completePorting(subscription.id) | ||
.build() | ||
return connectSession | ||
} | ||
|
||
beforeEach(() => { | ||
render(<div id="mount" />) | ||
}) | ||
|
||
describe('mounting', () => { | ||
it('mounts into a DOM selector', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
embed.mount('#mount') | ||
expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument() | ||
}) | ||
|
||
it('mounts into a DOM element', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
embed.mount(document.getElementById('mount')!) | ||
expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument() | ||
}) | ||
}) | ||
|
||
describe('updating', () => { | ||
it('updates the embed', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
embed.mount('#mount') | ||
embed.update({}) | ||
expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument() | ||
}) | ||
|
||
it('fails to update an unmounted embed', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
expect(() => embed.update({})).toThrow(/an unmounted embed/i) | ||
}) | ||
}) | ||
|
||
describe('unmounting', () => { | ||
it('unmounts the embed', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
embed.mount('#mount') | ||
expect(document.getElementById('mount')).not.toBeEmptyDOMElement() | ||
embed.unmount() | ||
expect(document.getElementById('mount')).toBeEmptyDOMElement() | ||
}) | ||
|
||
it('fails to unmount an unmounted embed', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
expect(() => embed.unmount()).toThrow(/an unmounted embed/i) | ||
}) | ||
}) | ||
|
||
describe('initialization', () => { | ||
it('initializes with valid data', async () => { | ||
const csn = await createFixtures() | ||
const embed = await PortingEmbed(csn, { project }) | ||
|
||
expect(embed.mount).toBeDefined() | ||
expect(embed.update).toBeDefined() | ||
expect(embed.unmount).toBeDefined() | ||
expect(embed.on).toBeDefined() | ||
expect(embed.off).toBeDefined() | ||
}) | ||
|
||
it('throws without a project', async () => { | ||
const csn = await createFixtures() | ||
// @ts-expect-error Assume a JS-developer forgets the project | ||
const init = PortingEmbed(csn, {}) | ||
expect(init).rejects.toThrow(/NO_PROJECT/) | ||
}) | ||
|
||
it('throws with a wrong intent', async () => { | ||
const csn = connectSessionFactory | ||
// @ts-expect-error Unsupported intent type | ||
.params({ intent: { type: 'foo' } }) | ||
.build() | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/WRONG_INTENT/) | ||
}) | ||
|
||
it('throws with a non-existing subscription', async () => { | ||
const csn = connectSessionFactory.completePorting('sub_404').build() | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/NOT_FOUND/) | ||
}) | ||
|
||
it('throws without a porting', async () => { | ||
const sub = await subscriptionFactory.withoutPorting().create() | ||
const csn = connectSessionFactory.completePorting(sub.id).build() | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/NOT_FOUND/) | ||
}) | ||
|
||
describe('with porting status', () => { | ||
async function createWithStatus(status: PortingStatus) { | ||
const porting = portingFactory.params({ status }).build() | ||
const subscription = await subscriptionFactory | ||
.associations({ porting }) | ||
.create() | ||
const connectSession = connectSessionFactory | ||
.completePorting(subscription.id) | ||
.build() | ||
return connectSession | ||
} | ||
|
||
it('initializes with informationRequired', async () => { | ||
const csn = await createWithStatus('informationRequired') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).resolves.toBeDefined() | ||
}) | ||
|
||
it('initializes with declined', async () => { | ||
const csn = await createWithStatus('declined') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).resolves.toBeDefined() | ||
}) | ||
|
||
it('throws with draft', async () => { | ||
const csn = await createWithStatus('draft') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/UNSUPPORTED/) | ||
}) | ||
|
||
it('throws with requested', async () => { | ||
const csn = await createWithStatus('requested') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/UNSUPPORTED/) | ||
}) | ||
|
||
it('throws with completed', async () => { | ||
const csn = await createWithStatus('completed') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/UNSUPPORTED/) | ||
}) | ||
|
||
it('throws with canceled', async () => { | ||
const csn = await createWithStatus('canceled') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/UNSUPPORTED/) | ||
}) | ||
|
||
it('throws with expired', async () => { | ||
const csn = await createWithStatus('expired') | ||
const init = PortingEmbed(csn, { project }) | ||
expect(init).rejects.toThrow(/UNSUPPORTED/) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import mitt from 'mitt' | ||
import { render } from 'preact' | ||
|
||
import { fetchSubscription } from '../core/subscription' | ||
import { exchangeSessionWithToken } from '../core/token' | ||
import { ConnectSession, PortingStatus } from '../types' | ||
import { | ||
CustomizableEmbedProps, | ||
PortingEmbed as PortingEmbedComponent, | ||
} from './PortingEmbed' | ||
|
||
type PortingEmbedInit = { project: string } | ||
export type PortingEmbedOptions = CustomizableEmbedProps | ||
|
||
type Events = never | ||
|
||
export async function PortingEmbed( | ||
session: ConnectSession, | ||
{ | ||
options: initialOptions, | ||
project, | ||
}: { options?: PortingEmbedOptions } & PortingEmbedInit, | ||
) { | ||
// Ensure embed was initialized with proper options | ||
const { intent } = session | ||
assert( | ||
project, | ||
'NO_PROJECT: Cannot initialize PortingEmbed without a project.', | ||
) | ||
assert( | ||
intent.type === 'completePorting', | ||
`WRONG_INTENT: PortingEmbed must be initialized with the "completePorting" intent, but got "${intent.type}" instead.`, | ||
) | ||
|
||
// Get a user token and ensure that the ConnectSession is valid. | ||
const token = await exchangeSessionWithToken(session) | ||
|
||
let element: Element | null = null | ||
let options = initialOptions | ||
const emitter = mitt<Events>() | ||
|
||
// Fetch the necessary data before the embed can be mounted. While the embed | ||
// is loading, the embedder can show their own loading state. | ||
const subscription = await fetchSubscription( | ||
intent.completePorting.subscription, | ||
{ project, token }, | ||
) | ||
const { porting } = subscription | ||
assert(porting, 'NOT_FOUND: The given subscription has no porting.') | ||
|
||
const supportedPortingStatus: PortingStatus[] = [ | ||
'informationRequired', | ||
'declined', | ||
] | ||
assert( | ||
supportedPortingStatus.includes(porting.status), | ||
`UNSUPPORTED: Porting status "${porting.status}" is not supported by the embed.`, | ||
) | ||
|
||
/** | ||
* Mount the embed into a container. | ||
* @param container The HTML Element or selector in which the embed should be | ||
* mounted to. | ||
*/ | ||
const mount = (container: Element | string) => { | ||
assert(container, 'Cannot call mount() without specifying a container.') | ||
|
||
element = | ||
typeof container === 'string' | ||
? document.querySelector(container) | ||
: container | ||
assert(element, 'Element to mount to could not be found.') | ||
|
||
renderWithCurrentOptions() | ||
} | ||
|
||
/** | ||
* Unmount the mounted embed. | ||
*/ | ||
const unmount = () => { | ||
assert(element, 'Cannot call unmount() on an unmounted embed.') | ||
|
||
render(null, element) | ||
element = null | ||
} | ||
|
||
/** | ||
* Update the mounted embed with new options. | ||
* @param newOptions New options for the embed | ||
*/ | ||
const update = (newOptions: PortingEmbedOptions) => { | ||
assert(element, 'Cannot call update() on an unmounted embed.') | ||
|
||
options = newOptions | ||
renderWithCurrentOptions() | ||
} | ||
|
||
const renderWithCurrentOptions = () => { | ||
assert(element, 'No element present to render embed into.') | ||
|
||
render( | ||
<PortingEmbedComponent | ||
{...options} | ||
token={token} | ||
initialPorting={porting} | ||
/>, | ||
element, | ||
) | ||
} | ||
|
||
return { | ||
mount, | ||
update, | ||
unmount, | ||
on: emitter.on.bind(emitter), | ||
off: emitter.off.bind(emitter), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { assert } from '../assert' | ||
|
||
it('throws when it evaluates to false', () => { | ||
expect(() => assert(false, 'is false')).toThrowError('is false') | ||
expect(() => assert(null, 'is null')).toThrowError('is null') | ||
expect(() => assert(0, 'is zero')).toThrowError('is zero') | ||
expect(() => assert('', 'is empty')).toThrowError('is empty') | ||
}) | ||
|
||
it('does not throw when it evaluates to true', () => { | ||
expect(() => assert(true, 'is true')).not.toThrowError('is true') | ||
expect(() => assert({}, 'is object')).not.toThrowError('is object') | ||
expect(() => assert('string', 'is string')).not.toThrowError('is string') | ||
expect(() => assert(1, 'is 1')).not.toThrowError('is 1') | ||
expect(() => assert([], 'is array')).not.toThrowError('is array') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { subscriptionFactory } from '@/testing/factories/subscription' | ||
|
||
import { fetchSubscription } from '../subscription' | ||
|
||
const project = 'test_project' | ||
const token = 'test_token' | ||
|
||
it('returns an existing subscription', async () => { | ||
const sub = await subscriptionFactory.create() | ||
const result = await fetchSubscription(sub.id, { project, token }) | ||
expect(result).toEqual(sub) | ||
}) | ||
|
||
it('throws if the subscription does not exist', async () => { | ||
expect( | ||
fetchSubscription('sub_not_found', { project, token }), | ||
).rejects.toThrow(/SUB_NOT_FOUND/) | ||
}) |
Oops, something went wrong.