diff --git a/src/components/wizard/RemoteConfigStep.test.tsx b/src/components/wizard/RemoteConfigStep.test.tsx index 2677158..5af2e1e 100644 --- a/src/components/wizard/RemoteConfigStep.test.tsx +++ b/src/components/wizard/RemoteConfigStep.test.tsx @@ -2,6 +2,8 @@ // Covers BACK-01: Azure Blob config form — account + SAS/Key toggle, both values preserved // Covers BACK-02: Amazon S3 config form — access_key_id, secret_access_key, region // Covers BACK-03: S3-Compatible config form — same as S3 plus endpoint field +// Covers VALID-01: Format validation stubs (RED — validate property not implemented yet) +// Covers UX-01: Tooltip toggle stubs (RED — tooltipText property not implemented yet) import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -227,4 +229,174 @@ describe('RemoteConfigStep', () => { }); }); }); + + describe('VALID-01 — format validation', () => { + // RED stubs — validate property not on FieldDef yet; no regex chaining in buildZodSchema() + // These tests will fail until Plan 07-01 implements format validation + + // Azure account name tests + it('rejects account name with uppercase letters', async () => { + const user = userEvent.setup(); + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/storage account name/i)).toBeDefined(); + }); + await user.clear(screen.getByLabelText(/storage account name/i)); + await user.type(screen.getByLabelText(/storage account name/i), 'MyStorage'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.getByText(/3.{1,5}24 lowercase alphanumeric/i)).toBeDefined(); + }); + }); + + it('rejects account name shorter than 3 characters', async () => { + const user = userEvent.setup(); + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/storage account name/i)).toBeDefined(); + }); + await user.clear(screen.getByLabelText(/storage account name/i)); + await user.type(screen.getByLabelText(/storage account name/i), 'ab'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.getByText(/3.{1,5}24 lowercase alphanumeric/i)).toBeDefined(); + }); + }); + + it('accepts valid account name (no format error)', async () => { + const user = userEvent.setup(); + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/storage account name/i)).toBeDefined(); + }); + await user.type(screen.getByLabelText(/storage account name/i), 'mystorageaccount'); + // Fill SAS URL field so form can submit without other required-field errors + await user.type(screen.getByLabelText(/sas url/i), 'https://mystorage.blob.core.windows.net/?sv=test'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.queryByText(/3.{1,5}24 lowercase alphanumeric/i)).toBeNull(); + }); + }); + + // S3 region tests + it('rejects S3 region with invalid format (spaces)', async () => { + const user = userEvent.setup(); + renderWithBackend('s3'); + await waitFor(() => { + expect(screen.getByLabelText(/region/i)).toBeDefined(); + }); + await user.type(screen.getByLabelText(/region/i), 'us east 1'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.getByText(/valid aws region format/i)).toBeDefined(); + }); + }); + + it('accepts valid S3 region (no format error)', async () => { + const user = userEvent.setup(); + renderWithBackend('s3'); + await waitFor(() => { + expect(screen.getByLabelText(/region/i)).toBeDefined(); + }); + // Fill all required S3 fields so form can submit + await user.type(screen.getByLabelText(/access key id/i), 'AKIAIOSFODNN7EXAMPLE'); + await user.type(screen.getByLabelText(/secret access key/i), 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'); + await user.type(screen.getByLabelText(/region/i), 'us-east-1'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.queryByText(/valid aws region format/i)).toBeNull(); + }); + }); + + // GCS project_number tests + it('rejects GCS project_number with non-digits', async () => { + const user = userEvent.setup(); + renderWithBackend('gcs' as BackendType); + await waitFor(() => { + expect(screen.getByLabelText(/project number/i)).toBeDefined(); + }); + await user.type(screen.getByLabelText(/project number/i), 'abc'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.getByText(/digits only/i)).toBeDefined(); + }); + }); + + it('accepts valid GCS project_number (no format error)', async () => { + const user = userEvent.setup(); + renderWithBackend('gcs' as BackendType); + await waitFor(() => { + expect(screen.getByLabelText(/project number/i)).toBeDefined(); + }); + await user.type(screen.getByLabelText(/project number/i), '123456789'); + await user.click(screen.getByRole('button', { name: /next/i })); + await waitFor(() => { + expect(screen.queryByText(/digits only/i)).toBeNull(); + }); + }); + }); + + describe('UX-01 — contextual tooltips', () => { + // RED stubs — tooltipText not on FieldDef yet; no ⓘ buttons rendered + // These tests will fail until Plan 07-02 implements tooltip toggle + + it('renders ⓘ button on azureblob sas_url field', async () => { + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/sas url/i)).toBeDefined(); + }); + // ⓘ button does not exist yet — will throw "Unable to find role 'button'" + expect(screen.getByRole('button', { name: /more info about sas url/i })).toBeDefined(); + }); + + it('shows tooltip panel when ⓘ is clicked on sas_url', async () => { + const user = userEvent.setup(); + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/sas url/i)).toBeDefined(); + }); + // ⓘ button does not exist yet — getByRole will throw + await user.click(screen.getByRole('button', { name: /more info about sas url/i })); + await waitFor(() => { + // Tooltip text should be visible after clicking ⓘ + expect(screen.getByText(/shared access signature/i)).toBeDefined(); + }); + }); + + it('hides tooltip panel when ⓘ is clicked again', async () => { + const user = userEvent.setup(); + renderWithBackend('azureblob'); + await waitFor(() => { + expect(screen.getByLabelText(/sas url/i)).toBeDefined(); + }); + // ⓘ button does not exist yet — getByRole will throw + await user.click(screen.getByRole('button', { name: /more info about sas url/i })); + await waitFor(() => { + expect(screen.getByText(/shared access signature/i)).toBeDefined(); + }); + await user.click(screen.getByRole('button', { name: /more info about sas url/i })); + await waitFor(() => { + expect(screen.queryByText(/shared access signature/i)).toBeNull(); + }); + }); + + it('renders ⓘ button on SFTP auth method section', async () => { + renderWithBackend('sftp' as BackendType); + await waitFor(() => { + expect(screen.getByLabelText(/host/i)).toBeDefined(); + }); + // ⓘ button does not exist yet — will throw "Unable to find role 'button'" + expect(screen.getByRole('button', { name: /more info about authentication/i })).toBeDefined(); + }); + + it('renders ⓘ button on OneDrive token field', async () => { + renderWithBackend('onedrive' as BackendType); + await waitFor(() => { + expect(screen.getByLabelText(/oauth token/i)).toBeDefined(); + }); + // ⓘ button does not exist yet — will throw "Unable to find role 'button'" + // Note: this matches any "more info about X" button on the OneDrive form + expect(screen.getByRole('button', { name: /more info about oauth token/i })).toBeDefined(); + }); + }); });