Skip to content

Commit

Permalink
Setup base for PortingEmbed (#1)
Browse files Browse the repository at this point in the history
- Add new PortingEmbed
- Add safeguards that the embed is initialized with the correct data
- Exchange session token with user token
- Prefetch the subscription and ensure a supported porting is present
- Renamed `lib/main.ts` to `lib/index.ts` because "main" is just another
word for "index", and "index" is the more conventional name.
- Add fixtures with [fishery](https://github.com/thoughtbot/fishery) for
better testing
  • Loading branch information
timomeh authored Feb 20, 2024
1 parent 50e5444 commit 56e2ba9
Show file tree
Hide file tree
Showing 31 changed files with 993 additions and 24 deletions.
10 changes: 10 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,15 @@ module.exports = {
},
],
'simple-import-sort/imports': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
ignoreRestSiblings: true,
varsIgnorePattern: '^_',
caughtErrors: 'all',
},
],
},
}
19 changes: 19 additions & 0 deletions lib/PortingEmbed/PortingEmbed.tsx
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>
}
22 changes: 22 additions & 0 deletions lib/PortingEmbed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Porting Embed

## Usage

```ts
import { PortingEmbed } from '@gigs/gigs-embeds-js'

// Obtain a ConnectSession from your own backend
const connectSession = await fetchConnectSession()

const embed = await PortingEmbed(connectSession, { project: 'your-project' })
embed.mount(document.getElementById('gigsEmbedMount'))

// can also use a selector:
// embed.mount('#gigsEmbedMount')

// Here be more dragons

// ---
// index.html
<div id="gigsEmbedMount"></div>
```
20 changes: 20 additions & 0 deletions lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { portingFactory } from '@/testing/factories/porting'

import { PortingEmbed } from '../PortingEmbed'

export default {
title: 'Porting Embed/Base',
component: PortingEmbed,
tags: ['autodocs'],
argTypes: {
token: { control: 'text' },
initialPorting: { control: 'object' },
},
}

export const Primary = {
args: {
token: 'abc:123',
initialPorting: portingFactory.build(),
},
}
12 changes: 12 additions & 0 deletions lib/PortingEmbed/__tests__/PortingEmbed.test.tsx
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()
})
181 changes: 181 additions & 0 deletions lib/PortingEmbed/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 the project is missing in a non-typechecked usage
const init = PortingEmbed(csn, {})
expect(init).rejects.toThrow(/NO_PROJECT/)
})

it('throws with the wrong ConnectSession', async () => {
expect(PortingEmbed(null, { project })).rejects.toThrow(/INVALID_SESSION/)
expect(PortingEmbed({}, { project })).rejects.toThrow(/INVALID_SESSION/)
expect(PortingEmbed({ secret: 'foo' }, { project })).rejects.toThrow(
/INVALID_SESSION/,
)
})

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(/INVALID_SESSION/)
})

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/)
})
})
})
Loading

0 comments on commit 56e2ba9

Please sign in to comment.