|
| 1 | +// Copyright (c) 2026 Sidero Labs, Inc. |
| 2 | +// |
| 3 | +// Use of this software is governed by the Business Source License |
| 4 | +// included in the LICENSE file. |
| 5 | +import { stat } from 'node:fs/promises' |
| 6 | + |
| 7 | +import { faker } from '@faker-js/faker' |
| 8 | +import { milliseconds } from 'date-fns' |
| 9 | +import { load } from 'js-yaml' |
| 10 | + |
| 11 | +import { expect, test } from '../auth_fixtures' |
| 12 | + |
| 13 | +test.describe.configure({ mode: 'parallel' }) |
| 14 | + |
| 15 | +test('Download installation media', async ({ page }, testInfo) => { |
| 16 | + test.slow() |
| 17 | + |
| 18 | + await test.step('Go to wizard', async () => { |
| 19 | + await page.goto('/') |
| 20 | + await page.getByRole('link', { name: 'Download Installation Media' }).click() |
| 21 | + |
| 22 | + const firstStepHeading = page.getByRole('heading', { name: 'Create New Media' }) |
| 23 | + |
| 24 | + const isVisible = await firstStepHeading |
| 25 | + .waitFor({ timeout: milliseconds({ seconds: 2 }) }) |
| 26 | + .then(() => true) |
| 27 | + .catch(() => false) |
| 28 | + |
| 29 | + // Only click "Create New" if we don't auto redirect (redirects directly to wizard if no configs exist) |
| 30 | + if (!isVisible) { |
| 31 | + await page.getByRole('link', { name: 'Create New' }).click() |
| 32 | + } |
| 33 | + |
| 34 | + await expect(firstStepHeading).toBeVisible() |
| 35 | + }) |
| 36 | + |
| 37 | + await test.step('Entry step', async () => { |
| 38 | + await page.getByRole('radio', { name: 'Bare-metal Machine' }).click() |
| 39 | + await expect(page.getByRole('radio', { name: 'Bare-metal Machine' })).toBeChecked() |
| 40 | + |
| 41 | + await page.getByRole('link', { name: 'Next' }).click() |
| 42 | + }) |
| 43 | + |
| 44 | + await test.step('Talos version step', async () => { |
| 45 | + await page.getByRole('combobox', { name: 'Choose Talos Linux Version' }).click() |
| 46 | + await page.getByRole('option', { name: '1.12.0' }).click() |
| 47 | + await expect(page.getByRole('combobox', { name: 'Choose Talos Linux Version' })).toHaveText( |
| 48 | + '1.12.0', |
| 49 | + ) |
| 50 | + |
| 51 | + await page.getByRole('combobox', { name: 'Join Token' }).click() |
| 52 | + await page.getByRole('option', { name: 'initial token' }).click() |
| 53 | + await expect(page.getByRole('combobox', { name: 'Join Token' })).toHaveText('initial token') |
| 54 | + |
| 55 | + await page.getByText('Tunnel Omni management').click() |
| 56 | + await expect(page.getByRole('checkbox', { name: 'Tunnel Omni management' })).toBeChecked() |
| 57 | + |
| 58 | + await page.getByRole('button', { name: 'new label' }).click() |
| 59 | + await page.getByRole('textbox').first().fill('foo:bar') |
| 60 | + await page.getByRole('textbox').first().press('Enter') |
| 61 | + await expect(page.getByRole('button', { name: 'foo:bar' })).toBeVisible() |
| 62 | + |
| 63 | + await page.getByRole('link', { name: 'Next' }).click() |
| 64 | + }) |
| 65 | + |
| 66 | + await test.step('Architecture step', async () => { |
| 67 | + await page.getByRole('radio', { name: 'arm64' }).click() |
| 68 | + await expect(page.getByRole('radio', { name: 'arm64' })).toBeChecked() |
| 69 | + |
| 70 | + await page.getByRole('checkbox', { name: 'SecureBoot' }).click() |
| 71 | + await expect(page.getByRole('checkbox', { name: 'SecureBoot' })).toBeChecked() |
| 72 | + |
| 73 | + await page.getByRole('link', { name: 'Next' }).click() |
| 74 | + }) |
| 75 | + |
| 76 | + await test.step('System extensions step', async () => { |
| 77 | + await page.getByPlaceholder('Search').fill('hello') |
| 78 | + await page.getByText('siderolabs/hello-world-service').click() |
| 79 | + await expect( |
| 80 | + page.getByRole('checkbox', { name: 'siderolabs/hello-world-service' }), |
| 81 | + ).toBeChecked() |
| 82 | + |
| 83 | + await page.getByRole('link', { name: 'Next' }).click() |
| 84 | + }) |
| 85 | + |
| 86 | + await test.step('Extra args step', async () => { |
| 87 | + await page.getByRole('radio', { name: 'Auto' }).click() |
| 88 | + await page.locator('.flex.max-h-full').first().click() |
| 89 | + |
| 90 | + await page |
| 91 | + .getByRole('textbox', { name: 'Extra kernel command line' }) |
| 92 | + .fill(`-console console=tty0`) |
| 93 | + |
| 94 | + await page.getByRole('link', { name: 'Next' }).click() |
| 95 | + }) |
| 96 | + |
| 97 | + const savedPresetName = `e2e-media-${faker.string.alphanumeric(8)}` |
| 98 | + |
| 99 | + let schematicId: string |
| 100 | + |
| 101 | + await test.step('Confirmation step', async () => { |
| 102 | + await page.getByRole('button', { name: 'Copy schematic ID' }).click() |
| 103 | + |
| 104 | + schematicId = await page.evaluate(() => navigator.clipboard.readText()) |
| 105 | + expect(schematicId, 'Expect schematic ID to be valid').toMatch(/[a-zA-Z0-9]{64}/) |
| 106 | + |
| 107 | + await page.getByRole('button', { name: 'Copy schematic YAML' }).click() |
| 108 | + |
| 109 | + const schematicYml = await page.evaluate(() => navigator.clipboard.readText()) |
| 110 | + const parsedSchematicYml = load(schematicYml) |
| 111 | + await testInfo.attach('schematic.yaml', { |
| 112 | + body: schematicYml, |
| 113 | + contentType: 'application/yaml', |
| 114 | + }) |
| 115 | + |
| 116 | + expect(parsedSchematicYml, 'Expect YAML to match expected shape').toEqual({ |
| 117 | + customization: { |
| 118 | + extraKernelArgs: [ |
| 119 | + expect.stringContaining('siderolink.api=grpc://'), |
| 120 | + expect.stringContaining('talos.events.sink='), |
| 121 | + expect.stringContaining('talos.logging.kernel='), |
| 122 | + '-console', |
| 123 | + 'console=tty0', |
| 124 | + ], |
| 125 | + meta: [{ key: 12, value: expect.any(String) }], |
| 126 | + systemExtensions: { |
| 127 | + officialExtensions: ['siderolabs/hello-world-service'], |
| 128 | + }, |
| 129 | + }, |
| 130 | + }) |
| 131 | + |
| 132 | + await expect( |
| 133 | + page.getByText( |
| 134 | + `https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.iso`, |
| 135 | + ), |
| 136 | + ).toBeVisible() |
| 137 | + |
| 138 | + await expect( |
| 139 | + page.getByText( |
| 140 | + `https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.raw.zst`, |
| 141 | + ), |
| 142 | + ).toBeVisible() |
| 143 | + |
| 144 | + await expect( |
| 145 | + page.getByText(`https://pxe.factory.talos.dev/${schematicId}/1.12.0/metal-arm64-secureboot`), |
| 146 | + ).toBeVisible() |
| 147 | + |
| 148 | + await page.getByRole('button', { name: 'Save' }).click() |
| 149 | + await page.getByRole('textbox', { name: 'Name:' }).fill(savedPresetName) |
| 150 | + await page.getByRole('textbox', { name: 'Name:' }).click() |
| 151 | + await page.getByLabel('Save preset').getByRole('button', { name: 'Save' }).click() |
| 152 | + await page.getByRole('link', { name: 'Finished' }).click() |
| 153 | + }) |
| 154 | + |
| 155 | + const presetRow = page.getByRole('row', { name: savedPresetName }) |
| 156 | + |
| 157 | + await test.step('Download the image', async () => { |
| 158 | + await presetRow.getByLabel('download').click() |
| 159 | + |
| 160 | + const isoRow = page.getByRole('row', { name: 'SecureBoot ISO' }) |
| 161 | + await isoRow.getByLabel('copy link').click() |
| 162 | + |
| 163 | + const isoLink = await page.evaluate(() => navigator.clipboard.readText()) |
| 164 | + expect(isoLink).toBe( |
| 165 | + `https://factory.talos.dev/image/${schematicId}/1.12.0/metal-arm64-secureboot.iso`, |
| 166 | + ) |
| 167 | + |
| 168 | + await isoRow.getByLabel('download').click() |
| 169 | + |
| 170 | + const [download] = await Promise.all([ |
| 171 | + page.waitForEvent('download'), |
| 172 | + expect(page.getByText('Generating Image')).toBeVisible(), |
| 173 | + ]) |
| 174 | + |
| 175 | + const filePath = testInfo.outputPath(download.suggestedFilename()) |
| 176 | + await download.saveAs(filePath) |
| 177 | + |
| 178 | + const { size } = await stat(filePath) |
| 179 | + |
| 180 | + expect(size, 'Expect ISO to be at least 50MB').toBeGreaterThan(50 * 1024 * 1024) |
| 181 | + |
| 182 | + await page.getByRole('button', { name: 'Close', exact: true }).click() |
| 183 | + }) |
| 184 | + |
| 185 | + await test.step('Delete the image', async () => { |
| 186 | + await presetRow.getByLabel('delete').click() |
| 187 | + |
| 188 | + await expect( |
| 189 | + page.getByText(`Are you sure you want to delete preset "${savedPresetName}"?`), |
| 190 | + ).toBeVisible() |
| 191 | + |
| 192 | + await page.getByRole('button', { name: 'Confirm' }).click() |
| 193 | + |
| 194 | + await expect(page.getByText(`Deleted preset ${savedPresetName}`)).toBeVisible() |
| 195 | + await expect(presetRow).toBeHidden() |
| 196 | + }) |
| 197 | +}) |
0 commit comments