Skip to content

Commit 3e318a2

Browse files
committed
almost done with 3
1 parent 05c9969 commit 3e318a2

File tree

4 files changed

+191
-25
lines changed

4 files changed

+191
-25
lines changed

exercises/03.optimistic/01.solution.optimistic/optimistic.test.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import './index.tsx'
66
await testStep('Initial ship details are loaded', async () => {
77
await screen.findByRole('heading', { name: /Dreadnought/i })
88
await waitFor(
9-
() => expect(screen.queryByText('loading')).not.toBeInTheDocument(),
9+
() => expect(screen.queryAllByText('loading')).toHaveLength(0),
1010
{ timeout: 5000 },
1111
)
1212
})
@@ -18,32 +18,48 @@ await testStep('Create form renders correctly', async () => {
1818
expect(screen.getByRole('button', { name: /Create/i })).toBeInTheDocument()
1919
})
2020

21-
await testStep('Optimistic UI updates when creating a new ship', async () => {
22-
const shipName = 'New Test Ship'
23-
const topSpeed = '9999'
24-
25-
fireEvent.change(screen.getByLabelText('Ship Name'), {
21+
const shipName = 'New Test Ship'
22+
const topSpeed = '9999'
23+
await testStep('Form submission functions', async () => {
24+
fireEvent.change(screen.getByLabelText(/Ship Name/i), {
2625
target: { value: shipName },
2726
})
28-
fireEvent.change(screen.getByLabelText('Top Speed'), {
27+
fireEvent.change(screen.getByLabelText(/Top Speed/i), {
2928
target: { value: topSpeed },
3029
})
3130

32-
// Mock file input
33-
const file = new File(['dummy content'], 'test.png', { type: 'image/png' })
34-
fireEvent.change(screen.getByLabelText('Image'), {
35-
target: { files: [file] },
36-
})
31+
// We can't actually select files via JavaScript, so we need to remove the required attribute
32+
const imageInput = screen.getByLabelText(/Image/i)
33+
imageInput.removeAttribute('required')
3734

3835
fireEvent.click(screen.getByRole('button', { name: /Create/i }))
36+
})
3937

38+
await testStep('Optimistic UI updates when creating a new ship', async () => {
39+
// wait just a bit for the optimistic update to happen
40+
await new Promise((resolve) => setTimeout(resolve, 100))
4041
// Check for optimistic update
41-
await screen.findByRole('heading', { name: new RegExp(shipName, 'i') })
42-
await screen.findByText(new RegExp(topSpeed, 'i'))
43-
44-
// Wait for the actual data to load
45-
await waitFor(
46-
() => expect(screen.queryByText('...')).not.toBeInTheDocument(),
47-
{ timeout: 5000 },
48-
)
42+
expect(
43+
screen.getByRole('heading', { name: new RegExp(shipName, 'i') }),
44+
'🚨 The optimistic update for the heading title is missing',
45+
).toBeInTheDocument()
46+
expect(
47+
screen.getByText(new RegExp(topSpeed, 'i'), { exact: false }),
48+
'🚨 The optimistic update for the top speed is missing',
49+
).toBeInTheDocument()
50+
// can't verify the image because we can't select files via JavaScript
4951
})
52+
53+
await testStep(
54+
'When the form submission succeeds, the final result is displayed',
55+
async () => {
56+
// Wait for the actual data to load
57+
await waitFor(
58+
() => expect(screen.queryByText('...')).not.toBeInTheDocument(),
59+
{ timeout: 5000 },
60+
)
61+
62+
await screen.findByRole('heading', { name: new RegExp(shipName, 'i') })
63+
await screen.findByText(new RegExp(topSpeed, 'i'))
64+
},
65+
)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2+
const { screen, waitFor, fireEvent } = dtl
3+
4+
import './index.tsx'
5+
6+
await testStep('Initial ship details are loaded', async () => {
7+
await screen.findByRole('heading', { name: /Dreadnought/i })
8+
await waitFor(
9+
() => expect(screen.queryAllByText('loading')).toHaveLength(0),
10+
{ timeout: 5000 },
11+
)
12+
})
13+
14+
const createButton = await testStep(
15+
'Create form renders correctly',
16+
async () => {
17+
expect(screen.getByLabelText('Ship Name')).toBeInTheDocument()
18+
expect(screen.getByLabelText('Top Speed')).toBeInTheDocument()
19+
expect(screen.getByLabelText('Image')).toBeInTheDocument()
20+
const createButton = screen.getByRole('button', { name: /Create/i })
21+
expect(createButton).toBeInTheDocument()
22+
return createButton
23+
},
24+
)
25+
26+
const shipName = 'New Test Ship'
27+
const topSpeed = '9999'
28+
await testStep('Form submission functions', async () => {
29+
fireEvent.change(screen.getByLabelText(/Ship Name/i), {
30+
target: { value: shipName },
31+
})
32+
fireEvent.change(screen.getByLabelText(/Top Speed/i), {
33+
target: { value: topSpeed },
34+
})
35+
36+
// We can't actually select files via JavaScript, so we need to remove the required attribute
37+
const imageInput = screen.getByLabelText(/Image/i)
38+
imageInput.removeAttribute('required')
39+
40+
fireEvent.click(createButton)
41+
})
42+
43+
await testStep(
44+
'Create button shows "Creating..." during submission',
45+
async () => {
46+
await waitFor(() => {
47+
expect(
48+
createButton,
49+
'🚨 The create button should show "Creating..." when the form is submitted',
50+
).toHaveTextContent('Creating...')
51+
})
52+
},
53+
)
54+
55+
await testStep('Create button shows "Create" after submission', async () => {
56+
await waitFor(
57+
() => {
58+
expect(
59+
createButton,
60+
'🚨 The create button should show "Create" when the request is complete',
61+
).toHaveTextContent('Create')
62+
},
63+
{ timeout: 5000 },
64+
)
65+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { expect, testStep, dtl } from '@epic-web/workshop-utils/test'
2+
const { screen, waitFor, fireEvent } = dtl
3+
4+
import './index.tsx'
5+
6+
await testStep(
7+
'Create button shows correct messages during form submission',
8+
async () => {
9+
const createButton = screen.getByRole('button', { name: /Create/i })
10+
expect(createButton).toBeInTheDocument()
11+
12+
// Mock form submission
13+
fireEvent.click(createButton)
14+
15+
// Check for "Creating..." message
16+
await waitFor(() => {
17+
expect(
18+
screen.getByRole('button', { name: /Creating.../i }),
19+
).toBeInTheDocument()
20+
})
21+
22+
// Check for "Created! Loading..." message
23+
await waitFor(
24+
() => {
25+
expect(
26+
screen.getByRole('button', { name: /Created! Loading.../i }),
27+
).toBeInTheDocument()
28+
},
29+
{ timeout: 5000 },
30+
)
31+
32+
// Wait for submission to complete and button to return to initial state
33+
await screen.findByRole('button', { name: /Create/i }, { timeout: 5000 })
34+
},
35+
)
36+
37+
await testStep('Optimistic UI updates with correct messages', async () => {
38+
const shipName = 'Message Test Ship'
39+
const topSpeed = '8888'
40+
41+
fireEvent.change(screen.getByLabelText('Ship Name'), {
42+
target: { value: shipName },
43+
})
44+
fireEvent.change(screen.getByLabelText('Top Speed'), {
45+
target: { value: topSpeed },
46+
})
47+
48+
// Mock file input
49+
const file = new File(['dummy content'], 'test.png', { type: 'image/png' })
50+
fireEvent.change(screen.getByLabelText('Image'), {
51+
target: { files: [file] },
52+
})
53+
54+
fireEvent.click(screen.getByRole('button', { name: /Create/i }))
55+
56+
// Check for optimistic update with "Creating..." message
57+
await screen.findByRole('heading', { name: new RegExp(shipName, 'i') })
58+
await screen.findByText(new RegExp(topSpeed, 'i'))
59+
expect(
60+
screen.getByRole('button', { name: /Creating.../i }),
61+
).toBeInTheDocument()
62+
63+
// Check for "Created! Loading..." message
64+
await waitFor(
65+
() => {
66+
expect(
67+
screen.getByRole('button', { name: /Created! Loading.../i }),
68+
).toBeInTheDocument()
69+
},
70+
{ timeout: 5000 },
71+
)
72+
73+
// Wait for the actual data to load and button to return to initial state
74+
await screen.findByRole('button', { name: /Create/i }, { timeout: 5000 })
75+
})

shared/ship-api-utils.server.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,30 @@ export async function createShip(request: Request) {
6565
const image = formData.get('image')
6666
const topSpeed = Number(formData.get('topSpeed'))
6767
invariantResponse(typeof name === 'string' && name, 'Name incorrect type')
68-
invariantResponse(image instanceof File, 'Image incorrect type')
68+
invariantResponse(!image || image instanceof File, 'Image incorrect type')
6969
invariantResponse(
7070
typeof topSpeed === 'number' && topSpeed,
7171
'Top speed incorrect type',
7272
)
7373

74-
const filePath = atRoot('public', 'img', 'custom-ships', image.name)
74+
// this is mostly for testing purposes. In the real app the image is required
75+
let imageName: string | null = null
76+
if (image instanceof File && image.name) {
77+
imageName = image.name
78+
const filePath = atRoot('public', 'img', 'custom-ships', image.name)
7579

76-
await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
77-
await fs.promises.writeFile(filePath, Buffer.from(await image.arrayBuffer()))
80+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
81+
await fs.promises.writeFile(
82+
filePath,
83+
Buffer.from(await image.arrayBuffer()),
84+
)
85+
}
7886

7987
const ship = {
8088
name,
81-
image: `/img/custom-ships/${image.name}`,
89+
image: imageName
90+
? `/img/custom-ships/${imageName}`
91+
: `/img/ships/battleship.webp`,
8292
topSpeed,
8393
weapons: [],
8494
}

0 commit comments

Comments
 (0)