diff --git a/src/routes/api/domains/[name]/+server.ts b/src/routes/api/domains/[name]/+server.ts
index c9f03ae1..35339e5e 100644
--- a/src/routes/api/domains/[name]/+server.ts
+++ b/src/routes/api/domains/[name]/+server.ts
@@ -5,6 +5,6 @@ export async function GET({ params: { name } }) {
return handleError(async () => {
const domain = await metaNamesSdk.domainRepository.find(name);
- return json({ domain: domain?.toJSON() });
+ return json({ domain: domain ? domain.toJSON() : null });
});
}
diff --git a/src/routes/api/proposals/voters/add/+server.ts b/src/routes/api/proposals/voters/add/+server.ts
deleted file mode 100644
index 62f2caf8..00000000
--- a/src/routes/api/proposals/voters/add/+server.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { metaNamesSdk } from '$lib/server';
-import { json } from '@sveltejs/kit';
-import { config } from 'src/lib';
-import { proposalsWalletPrivateKey } from 'src/lib/server/config';
-import { actionAddVotersPayload } from 'src/lib/proposal';
-
-export async function GET() {
- metaNamesSdk.setSigningStrategy('privateKey', proposalsWalletPrivateKey);
-
- const votingContractState = await metaNamesSdk.contractRepository.getState({
- contractAddress: config.tldMigrationProposalContractAddress
- });
- const fields = votingContractState.fieldsMap;
-
- const deadline = fields.get('deadline_utc_millis')?.asBN().toNumber();
- if (deadline && deadline < Date.now())
- return json({ error: 'Voting has ended' }, { status: 400 });
-
- const owners = await metaNamesSdk.domainRepository.getOwners();
- const voters =
- fields
- .get('voters')
- ?.setValue()
- .values.map((voter) => voter.addressValue().value.toString('hex')) ?? [];
-
- const newVoters = owners.filter((owner) => !voters.includes(owner)).slice(0, 50);
- if (newVoters.length === 0) return json({ newVoters }, { status: 200 });
-
- const votingContract = await metaNamesSdk.contractRepository.getContract({
- contractAddress: config.tldMigrationProposalContractAddress
- });
- const payload = actionAddVotersPayload(votingContract.abi, newVoters);
-
- const { transactionHash } = await metaNamesSdk.contractRepository.createTransaction({
- contractAddress: config.tldMigrationProposalContractAddress,
- payload,
- gasCost: 'low'
- });
-
- metaNamesSdk.resetSigningStrategy();
-
- return json({ newVoters, transactionHash });
-}
diff --git a/src/routes/api/proposals/voters/remove/+server.ts b/src/routes/api/proposals/voters/remove/+server.ts
deleted file mode 100644
index d024e90f..00000000
--- a/src/routes/api/proposals/voters/remove/+server.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { metaNamesSdk } from '$lib/server';
-import { json } from '@sveltejs/kit';
-import {
- proposalsWalletPrivateKey,
- tldMigrationProposalContractAddress
-} from 'src/lib/server/config';
-import { actionRemoveVotersPayload } from 'src/lib/proposal';
-
-export async function GET() {
- metaNamesSdk.setSigningStrategy('privateKey', proposalsWalletPrivateKey);
-
- const votingContractState = await metaNamesSdk.contractRepository.getState({
- contractAddress: tldMigrationProposalContractAddress
- });
- const fields = votingContractState.fieldsMap;
-
- const deadline = fields.get('deadline_utc_millis')?.asBN().toNumber();
- if (deadline && deadline < Date.now())
- return json({ error: 'Voting has ended' }, { status: 400 });
-
- const owners = await metaNamesSdk.domainRepository.getOwners();
- const voters =
- fields
- .get('voters')
- ?.setValue()
- .values.map((voter) => voter.addressValue().value.toString('hex')) ?? [];
-
- const votersToRemove = voters.filter((voter) => !owners.includes(voter)).slice(0, 50);
- if (votersToRemove.length === 0) return json({ newVoters: votersToRemove }, { status: 200 });
-
- const votingContract = await metaNamesSdk.contractRepository.getContract({
- contractAddress: tldMigrationProposalContractAddress
- });
- const payload = actionRemoveVotersPayload(votingContract.abi, votersToRemove);
-
- const { transactionHash } = await metaNamesSdk.contractRepository.createTransaction({
- contractAddress: tldMigrationProposalContractAddress,
- payload,
- gasCost: 'low'
- });
-
- metaNamesSdk.resetSigningStrategy();
-
- return json({ newVoters: votersToRemove, transactionHash });
-}
diff --git a/src/routes/proposals/tld-migration/+page.svelte b/src/routes/proposals/tld-migration/+page.svelte
deleted file mode 100644
index b4248c9f..00000000
--- a/src/routes/proposals/tld-migration/+page.svelte
+++ /dev/null
@@ -1,191 +0,0 @@
-
-
-
-
-
diff --git a/src/routes/proposals/tld-migration/+page.ts b/src/routes/proposals/tld-migration/+page.ts
deleted file mode 100644
index 7238fe80..00000000
--- a/src/routes/proposals/tld-migration/+page.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { config } from 'src/lib';
-import { getDeadline, getVotesResult } from 'src/lib/proposal';
-import { metaNamesSdkFactory } from 'src/lib/sdk.js';
-
-export async function load() {
- const state = await metaNamesSdkFactory().contractRepository.getState({
- contractAddress: config.tldMigrationProposalContractAddress
- });
-
- const deadlineInSeconds = getDeadline(state) / 1000;
- const result = getVotesResult(state);
-
- return { deadlineInSeconds, result };
-}
diff --git a/src/routes/proposals/tld-migration/Timer.svelte b/src/routes/proposals/tld-migration/Timer.svelte
deleted file mode 100644
index 83dc7ab3..00000000
--- a/src/routes/proposals/tld-migration/Timer.svelte
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
diff --git a/src/routes/register/[name]/+page.svelte b/src/routes/register/[name]/+page.svelte
index ce2b638d..db4b6443 100644
--- a/src/routes/register/[name]/+page.svelte
+++ b/src/routes/register/[name]/+page.svelte
@@ -88,7 +88,7 @@
+
{#if $isDomainPresent === undefined}
{:else}
diff --git a/tests/api/domains.spec.ts b/tests/api/domains.spec.ts
new file mode 100644
index 00000000..027df476
--- /dev/null
+++ b/tests/api/domains.spec.ts
@@ -0,0 +1,114 @@
+import { test, expect } from '@playwright/test';
+
+/**
+ * API tests for domain endpoints.
+ * These tests call the actual blockchain via the SDK.
+ */
+
+test.describe('Feature 9: API Endpoints - Domains', () => {
+ const baseUrl = 'http://localhost:4173';
+
+ test.describe('GET /api/domains/{name}/check', () => {
+ test('8.1 - Check availability returns correct structure for available domain', async ({
+ request
+ }) => {
+ const domainName = `notregistered${Date.now()}.mpc`;
+ const response = await request.get(`${baseUrl}/api/domains/${domainName}/check`);
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+
+ expect(typeof data.domainPresent).toBe('boolean');
+ expect(typeof data.parentPresent).toBe('boolean');
+ });
+
+ test('8.1b - Check returns domainPresent=true for registered domain', async ({ request }) => {
+ const domainName = 'test.mpc';
+ const response = await request.get(`${baseUrl}/api/domains/${domainName}/check`);
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+ expect(data.domainPresent).toBe(true);
+ });
+ });
+
+ test.describe('GET /api/domains/{name}', () => {
+ test('8.2 - Get domain details for existing domain returns full domain data', async ({
+ request
+ }) => {
+ const domainName = 'test.mpc';
+ const response = await request.get(`${baseUrl}/api/domains/${domainName}`);
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+ expect(data).toHaveProperty('domain');
+ expect(data.domain).not.toBeNull();
+ expect(data.domain).toHaveProperty('name');
+ expect(data.domain.name).toBe('test.mpc');
+ expect(data.domain).toHaveProperty('owner');
+ expect(typeof data.domain.owner).toBe('string');
+ });
+
+ test('8.2 - Get domain details for non-existent domain returns empty object', async ({
+ request
+ }) => {
+ const domainName = `nonexistent${Date.now()}.mpc`;
+ const response = await request.get(`${baseUrl}/api/domains/${domainName}`);
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+ // When domain is not found, find() returns null
+ expect(data.domain).toBeNull();
+ });
+ });
+
+ test.describe('GET /api/domains/recent', () => {
+ test('8.3 - Get recent domains returns non-empty array with domain names', async ({
+ request
+ }) => {
+ const response = await request.get(`${baseUrl}/api/domains/recent`, { timeout: 30000 });
+
+ expect(response.ok()).toBeTruthy();
+
+ const data = await response.json();
+ expect(Array.isArray(data)).toBeTruthy();
+ // Testnet has 400+ domains — recent list must not be empty
+ expect(data.length).toBeGreaterThan(0);
+
+ const firstDomain = data[0];
+ expect(firstDomain).toHaveProperty('name');
+ expect(typeof firstDomain.name).toBe('string');
+ expect(firstDomain.name.length).toBeGreaterThan(0);
+ });
+ });
+
+ test.describe('GET /api/domains/stats', () => {
+ test('8.4 - Get stats returns valid statistics', async ({ request }) => {
+ const response = await request.get(`${baseUrl}/api/domains/stats`, {
+ timeout: 30000
+ });
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+
+ // Stats structure
+ expect(typeof data).toBe('object');
+ expect(data).toHaveProperty('domainCount');
+ expect(data).toHaveProperty('ownerCount');
+ expect(data).toHaveProperty('recentDomains');
+
+ // Testnet has 400+ domains and multiple owners — counts must be positive
+ expect(typeof data.domainCount).toBe('number');
+ expect(typeof data.ownerCount).toBe('number');
+ expect(data.domainCount).toBeGreaterThan(0);
+ expect(data.ownerCount).toBeGreaterThan(0);
+
+ // recentDomains is an array of domain projections
+ expect(Array.isArray(data.recentDomains)).toBeTruthy();
+ for (const domain of data.recentDomains) {
+ expect(typeof domain.name).toBe('string');
+ expect(domain.name.length).toBeGreaterThan(0);
+ }
+ });
+ });
+});
diff --git a/tests/api/fees.spec.ts b/tests/api/fees.spec.ts
new file mode 100644
index 00000000..43a8921c
--- /dev/null
+++ b/tests/api/fees.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '@playwright/test';
+
+/**
+ * API tests for fee endpoints.
+ * These tests call the actual blockchain via the SDK (testnet).
+ *
+ * Uses TEST_COIN — the testnet token provisioned by the MetaNames protocol.
+ * This is the only coin that works on testnet for fee queries.
+ * Do not replace with BTC/ETH/USDC — these are mainnet-only.
+ */
+
+test.describe('Feature 9: API Endpoints - Fees', () => {
+ const baseUrl = 'http://localhost:4173';
+
+ test.describe('GET /api/register/{name}/fees/{coin}', () => {
+ test('8.5 - Get fees for TEST_COIN returns fee breakdown with numeric value', async ({
+ request
+ }) => {
+ const domainName = `testfees${Date.now()}`;
+
+ // Give blockchain time to respond on testnet
+ const response = await request.get(
+ `${baseUrl}/api/register/${domainName}/fees/TEST_COIN`,
+ { timeout: 30000 }
+ );
+
+ expect(response.ok()).toBeTruthy();
+ const data = await response.json();
+ expect(data).toHaveProperty('fees');
+ expect(typeof data.fees).toBe('string');
+ // Fees must be a parseable number > 0
+ expect(Number(data.fees)).toBeGreaterThan(0);
+ });
+
+ test('8.6 - Get fees for invalid coin returns error', async ({ request }) => {
+ const domainName = `testfees${Date.now()}`;
+ const invalidCoin = 'DEFINITELY_NOT_A_COIN_12345';
+
+ const response = await request.get(
+ `${baseUrl}/api/register/${domainName}/fees/${invalidCoin}`,
+ { timeout: 30000 }
+ );
+
+ // Should return 400 for invalid coin
+ expect(response.status()).toBeGreaterThanOrEqual(400);
+ const data = await response.json();
+ expect(data).toHaveProperty('error');
+ });
+ });
+});
diff --git a/tests/e2e/blockchain-ops.spec.ts b/tests/e2e/blockchain-ops.spec.ts
new file mode 100644
index 00000000..78293158
--- /dev/null
+++ b/tests/e2e/blockchain-ops.spec.ts
@@ -0,0 +1,176 @@
+import { test, expect, type Page } from '@playwright/test';
+import { loginOnCurrentPage, loginAtHome, spaNavigate } from './helpers';
+
+/**
+ * Blockchain operation tests: register domain, add/update/delete records.
+ * These submit real transactions to testnet.
+ *
+ * MUST run sequentially — testnet cannot process > 1 tx/second.
+ * Uses test.describe.serial() to enforce order.
+ */
+
+// Generate a unique 8-char domain name for this test run
+const domainName = `t${Date.now().toString(36).slice(-7)}`;
+const fullDomain = `${domainName}.mpc`;
+
+test.describe.serial('Blockchain Operations (sequential)', () => {
+ test.describe.configure({ retries: 1 }); // Allow 1 retry — blockchain txns can be slow
+ test.setTimeout(120_000); // 2 min per test — blockchain txns are slow
+
+ test('B1 - Register a new domain', async ({ page }) => {
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+ await loginOnCurrentPage(page);
+
+ // Wait for fee data to load
+ await expect(page.locator('[data-testid="total-fees"]')).toBeVisible({ timeout: 30000 });
+
+ // Click "Approve fees"
+ const approveBtn = page.locator('button:has-text("Approve fees")');
+ await expect(approveBtn).toBeVisible({ timeout: 10000 });
+ await expect(approveBtn).toBeEnabled({ timeout: 5000 });
+ await approveBtn.click();
+
+ // Wait for transaction to complete — snackbar appears
+ await expect(page.locator('text=New Transaction submitted')).toBeVisible({ timeout: 60000 });
+
+ // "Register domain" button should now be enabled
+ const registerBtn = page.locator('button:has-text("Register domain")');
+ await expect(registerBtn).toBeEnabled({ timeout: 30000 });
+
+ // Wait for testnet to process previous tx
+ await page.waitForTimeout(2000);
+
+ await registerBtn.click();
+
+ // Wait for success message and redirect to domain page
+ await expect(page.locator('text=Domain registered successfully')).toBeVisible({
+ timeout: 60000
+ });
+ await page.waitForURL(/\/domain\//, { timeout: 30000 });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+ });
+
+ test('B2 - Add a DNS record to the new domain', async ({ page }) => {
+ await page.goto(`/domain/${fullDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ // Click settings tab
+ const settingsTab = page.locator('role=tab[name="settings"]');
+ await expect(settingsTab).toBeVisible({ timeout: 10000 });
+ await settingsTab.click();
+
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ // Should show "No records found" for new domain
+ await expect(page.locator('text=No records found')).toBeVisible({ timeout: 5000 });
+
+ // Select record type "Bio"
+ const typeSelect = page.locator('.add-record select, .add-record .mdc-select').first();
+ await typeSelect.click();
+ await page.locator('.mdc-deprecated-list-item', { hasText: 'Bio' }).click();
+
+ // Fill record value
+ const valueInput = page.locator('.add-record .value input, .add-record .value textarea').first();
+ await valueInput.fill('Integration test bio');
+
+ // Click "Add record"
+ const addBtn = page.locator('button:has-text("Add record")');
+ await expect(addBtn).toBeEnabled({ timeout: 5000 });
+ await addBtn.click();
+
+ // Wait for transaction
+ await expect(page.locator('text=New Transaction submitted')).toBeVisible({ timeout: 60000 });
+
+ // Wait for testnet confirmation
+ await page.waitForTimeout(3000);
+
+ // Record should appear (Bio with our value inside disabled textbox)
+ await expect(page.locator('.record-container').first()).toBeVisible({ timeout: 15000 });
+ // Use toHaveValue on the disabled textbox directly (text= doesn't search disabled inputs)
+ await expect(page.locator('.record-container textarea[disabled]')).toHaveValue('Integration test bio');
+ });
+
+ test('B3 - Edit the DNS record', async ({ page }) => {
+ await page.goto(`/domain/${fullDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ // Navigate to settings
+ await page.locator('button:has-text("settings")').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ // Click edit on the Bio record
+ const editBtn = page.locator('[aria-label="edit-record"]').first();
+ await expect(editBtn).toBeVisible({ timeout: 5000 });
+ await editBtn.click();
+
+ // Save and cancel buttons should appear
+ await expect(page.locator('[aria-label="save-record"]').first()).toBeVisible({ timeout: 5000 });
+
+ // Clear and type new value
+ const textarea = page.locator('.record-container textarea').first();
+ await textarea.fill('Updated bio value');
+
+ // Click save
+ await page.locator('[aria-label="save-record"]').first().click();
+
+ // Wait for transaction
+ await expect(page.locator('text=New Transaction submitted')).toBeVisible({ timeout: 60000 });
+
+ // Wait for testnet confirmation
+ await page.waitForTimeout(3000);
+
+ // Edit button should reappear (back to view mode)
+ await expect(page.locator('[aria-label="edit-record"]').first()).toBeVisible({ timeout: 15000 });
+
+ // Value should be updated (check the disabled textarea)
+ await expect(page.locator('.record-container textarea[disabled]')).toHaveValue('Updated bio value');
+ });
+
+ test('B4 - Delete the DNS record', async ({ page }) => {
+ await page.goto(`/domain/${fullDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ // Navigate to settings tab (use tab role + text to be specific)
+ await page.locator('role=tab[name="settings"]').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ // Record should exist
+ await expect(page.locator('.record-container').first()).toBeVisible({ timeout: 5000 });
+
+ // Click delete
+ const deleteBtn = page.locator('[aria-label="delete-record"]').first();
+ await expect(deleteBtn).toBeVisible({ timeout: 5000 });
+ await deleteBtn.click();
+
+ // Confirmation dialog should appear
+ await expect(page.locator('text=Do you really want to remove the record')).toBeVisible({
+ timeout: 5000
+ });
+
+ // Click "Yes" to confirm
+ await page.locator('.mdc-dialog button:has-text("Yes")').click();
+
+ // Wait for transaction
+ await expect(page.locator('text=New Transaction submitted')).toBeVisible({ timeout: 60000 });
+
+ // Wait for testnet confirmation + page refresh
+ await page.waitForTimeout(3000);
+
+ // Reload and click settings tab to ensure we're on the right tab
+ await page.reload({ waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+ await loginOnCurrentPage(page);
+ await page.locator('role=tab[name="settings"]').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ // Record should be gone — "No records found" should show
+ await expect(page.locator('text=No records found')).toBeVisible({ timeout: 15000 });
+ });
+});
diff --git a/tests/e2e/dev-wallet.spec.ts b/tests/e2e/dev-wallet.spec.ts
new file mode 100644
index 00000000..5afa10f8
--- /dev/null
+++ b/tests/e2e/dev-wallet.spec.ts
@@ -0,0 +1,89 @@
+import { test, expect } from '@playwright/test';
+import { TEST_PRIVATE_KEY, loginOnCurrentPage } from './helpers';
+
+// Use the .mdc-top-app-bar__action-item class to target the header button specifically.
+// This avoids matching the ConnectionRequired body button on protected pages.
+const headerBtnSelector = 'button.mdc-top-app-bar__action-item';
+
+/**
+ * Helper: navigate to homepage, handle 500 errors from cold SvelteKit dev server.
+ */
+async function gotoHomeReliably(page: import('@playwright/test').Page) {
+ await page.goto('/', { waitUntil: 'networkidle' });
+ // CI: SvelteKit dev server can 500 on cold start. Detect and retry.
+ if (await page.locator('text=Internal Error').isVisible({ timeout: 1000 }).catch(() => false)) {
+ await page.waitForTimeout(2000);
+ await page.reload({ waitUntil: 'networkidle' });
+ }
+}
+
+test.describe('Feature: Dev Private Key Login (Wallet Menu)', () => {
+ test('should show dev key input in wallet menu on testnet', async ({ page }) => {
+ await gotoHomeReliably(page);
+
+ const connectBtn = page.locator(headerBtnSelector);
+ await expect(connectBtn).toBeVisible({ timeout: 15000 });
+
+ // Allow SvelteKit hydration to complete.
+ await page.waitForTimeout(2000);
+ await connectBtn.click();
+
+ // Wait for dev-key-input to be visible (15s timeout handles hydration delays).
+ await expect(page.locator('.dev-key-input')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator('.dev-key-connect')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('should have connect button disabled with empty key', async ({ page }) => {
+ await gotoHomeReliably(page);
+
+ const connectBtn = page.locator(headerBtnSelector);
+ await expect(connectBtn).toBeVisible({ timeout: 15000 });
+
+ // Allow SvelteKit hydration to complete.
+ await page.waitForTimeout(2000);
+ await connectBtn.click();
+
+ // Wait for dev-key-connect button to be visible (15s timeout handles hydration delays).
+ await expect(page.locator('.dev-key-connect')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator('.dev-key-connect')).toBeDisabled({ timeout: 5000 });
+ });
+
+ test('should login with valid private key and show address', async ({ page }) => {
+ await gotoHomeReliably(page);
+ await loginOnCurrentPage(page);
+
+ const connectBtn = page.locator(headerBtnSelector);
+ await expect(connectBtn).toContainText('...', { timeout: 10000 });
+ });
+
+ test('should disconnect wallet via menu', async ({ page }) => {
+ await gotoHomeReliably(page);
+ await loginOnCurrentPage(page);
+
+ const walletBtn = page.locator(headerBtnSelector);
+ await walletBtn.click();
+
+ const disconnectItem = page.locator('li', { hasText: 'Disconnect' }).first();
+ await expect(disconnectItem).toBeVisible({ timeout: 5000 });
+ await disconnectItem.click();
+
+ await expect(page.locator(headerBtnSelector)).toContainText('Connect', { timeout: 5000 });
+ });
+
+ test('should reject invalid private key (wrong length)', async ({ page }) => {
+ await gotoHomeReliably(page);
+
+ const connectBtn = page.locator(headerBtnSelector);
+ await expect(connectBtn).toBeVisible({ timeout: 15000 });
+
+ // Allow SvelteKit hydration to complete.
+ await page.waitForTimeout(2000);
+ await connectBtn.click();
+
+ // Wait for dev-key-input to be visible (15s timeout handles hydration delays).
+ await expect(page.locator('.dev-key-input')).toBeVisible({ timeout: 15000 });
+ await page.locator('.dev-key-input').fill('abc123');
+
+ await expect(page.locator('.dev-key-connect')).toBeDisabled();
+ });
+});
diff --git a/tests/e2e/dns-records.spec.ts b/tests/e2e/dns-records.spec.ts
new file mode 100644
index 00000000..0fc73e8b
--- /dev/null
+++ b/tests/e2e/dns-records.spec.ts
@@ -0,0 +1,108 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+/**
+ * Feature 7: DNS Records & Settings
+ * test.mpc is owned by the test wallet on testnet.
+ * Domain page structure (Profile, Whois, avatar) is covered in Feature 4.
+ * These tests focus on the Settings tab and record management.
+ */
+
+test.describe('Feature 7: DNS Records & Settings', () => {
+ const registeredDomain = 'test.mpc';
+
+ test.describe('Unauthenticated', () => {
+ test('7.1 - No tabs visible when not logged in', async ({ page }) => {
+ await page.goto(`/domain/${registeredDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await expect(page.locator('button:has-text("settings")')).not.toBeVisible();
+ await expect(page.locator('button:has-text("details")')).not.toBeVisible();
+ });
+ });
+
+ test.describe('Authenticated (owner)', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(`/domain/${registeredDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+ await loginOnCurrentPage(page);
+ });
+
+ test('7.2 - Both tabs visible after login', async ({ page }) => {
+ await expect(page.locator('button:has-text("settings")')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('button:has-text("details")')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('7.3 - Settings tab shows records section and add-record form', async ({ page }) => {
+ await page.locator('button:has-text("settings")').click();
+
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('.add-record')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('7.4 - Settings tab shows Renew and Transfer buttons with correct links', async ({
+ page
+ }) => {
+ await page.locator('button:has-text("settings")').click();
+
+ const renewBtn = page.locator('a:has-text("Renew")');
+ await expect(renewBtn).toBeVisible({ timeout: 5000 });
+ await expect(renewBtn).toHaveAttribute('href', `/domain/${registeredDomain}/renew`);
+
+ const transferBtn = page.locator('a:has-text("Transfer")');
+ await expect(transferBtn).toBeVisible({ timeout: 5000 });
+ await expect(transferBtn).toHaveAttribute('href', `/domain/${registeredDomain}/transfer`);
+ });
+
+ test('7.5 - Records visible with edit/delete buttons', async ({ page }) => {
+ await page.locator('button:has-text("settings")').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ // test.mpc has records (Bio, Price) — record containers must exist
+ await expect(page.locator('.record-container').first()).toBeVisible({ timeout: 5000 });
+
+ // Edit and delete buttons must be present
+ await expect(page.locator('[aria-label="edit-record"]').first()).toBeVisible({
+ timeout: 5000
+ });
+ await expect(page.locator('[aria-label="delete-record"]').first()).toBeVisible({
+ timeout: 5000
+ });
+ });
+
+ test('7.6 - Clicking edit shows save/cancel, cancel restores edit button', async ({
+ page
+ }) => {
+ await page.locator('button:has-text("settings")').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+
+ const editBtn = page.locator('[aria-label="edit-record"]').first();
+ await editBtn.click();
+
+ await expect(page.locator('[aria-label="save-record"]').first()).toBeVisible({
+ timeout: 5000
+ });
+ await expect(page.locator('[aria-label="cancel-edit"]').first()).toBeVisible({
+ timeout: 5000
+ });
+
+ // Cancel and verify edit button returns
+ await page.locator('[aria-label="cancel-edit"]').first().click();
+ await expect(editBtn).toBeVisible({ timeout: 5000 });
+ });
+
+ test('7.7 - Tab switching: details → settings → details', async ({ page }) => {
+ // Starts on details — Profile visible
+ await expect(page.locator('h5:has-text("Profile")').first()).toBeVisible({ timeout: 10000 });
+
+ // Switch to settings
+ await page.locator('button:has-text("settings")').click();
+ await expect(page.locator('.records')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('h5:has-text("Profile")').first()).not.toBeVisible();
+
+ // Switch back
+ await page.locator('button:has-text("details")').click();
+ await expect(page.locator('h5:has-text("Profile")').first()).toBeVisible({ timeout: 10000 });
+ });
+ });
+});
diff --git a/tests/e2e/domain-management.spec.ts b/tests/e2e/domain-management.spec.ts
new file mode 100644
index 00000000..f86f29f9
--- /dev/null
+++ b/tests/e2e/domain-management.spec.ts
@@ -0,0 +1,62 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+/**
+ * Feature 4: Domain Management
+ * test.mpc is a known registered domain on testnet, owned by the test wallet.
+ */
+
+test.describe('Feature 4: Domain Management', () => {
+ const registeredDomain = 'test.mpc';
+
+ test('4.1 - Domain page loads with name, avatar, profile, and whois', async ({ page }) => {
+ await page.goto(`/domain/${registeredDomain}`, { waitUntil: 'networkidle' });
+
+ // Domain heading with correct text
+ const domainHeading = page.locator('h5.domain');
+ await expect(domainHeading).toBeVisible({ timeout: 15000 });
+ await expect(domainHeading).toContainText(/test/i);
+
+ // Avatar SVG identicon
+ await expect(page.locator('.avatar svg').first()).toBeVisible({ timeout: 5000 });
+
+ // Profile section
+ await expect(page.locator('h5:has-text("Profile")')).toBeVisible({ timeout: 10000 });
+
+ // Short link chip
+ await expect(page.locator('.chip').filter({ hasText: 'link' }).first()).toBeVisible({
+ timeout: 5000
+ });
+
+ // Whois section with owner and expiry
+ await expect(page.locator('h5:has-text("Whois")')).toBeVisible({ timeout: 10000 });
+ await expect(page.getByRole('button', { name: /Owner 0/i })).toBeVisible({ timeout: 5000 });
+ await expect(page.getByRole('button', { name: /Expires/i })).toBeVisible({ timeout: 5000 });
+ });
+
+ test('4.2 - Non-existent domain redirects to register page', async ({ page }) => {
+ const fakeDomain = `nonexistent${Date.now()}.mpc`;
+ await page.goto(`/domain/${fakeDomain}`, { waitUntil: 'networkidle' });
+
+ // App calls find() → null → redirects to /register/{name}
+ await page.waitForURL(/\/register\//, { timeout: 15000 });
+ });
+
+ test('4.3 - Owner sees TabBar with details and settings tabs', async ({ page }) => {
+ await page.goto(`/domain/${registeredDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('button:has-text("details")')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('button:has-text("settings")')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('4.4 - Non-owner does NOT see TabBar', async ({ page }) => {
+ await page.goto(`/domain/${registeredDomain}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+
+ await expect(page.locator('button:has-text("details")')).not.toBeVisible();
+ await expect(page.locator('button:has-text("settings")')).not.toBeVisible();
+ });
+});
diff --git a/tests/e2e/domain-registration.spec.ts b/tests/e2e/domain-registration.spec.ts
new file mode 100644
index 00000000..8febc790
--- /dev/null
+++ b/tests/e2e/domain-registration.spec.ts
@@ -0,0 +1,118 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+test.describe('Feature 3: Domain Registration', () => {
+ test('3.1 - Checkout form visible for available domain', async ({ page }) => {
+ const domainName = `e2ereg${Date.now()}`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+ });
+
+ test('3.2 - Payment token dropdown visible', async ({ page }) => {
+ const domainName = `e2etoken${Date.now()}`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+
+ await expect(page.locator('[data-testid="payment-token-section"]')).toBeVisible({
+ timeout: 10000
+ });
+ await expect(page.locator('[data-testid="payment-token-select"]')).toBeVisible({
+ timeout: 5000
+ });
+ });
+
+ test('3.3 - Year selector increments and decrements', async ({ page }) => {
+ const domainName = `e2eyears${Date.now()}`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+
+ const addBtn = page.locator('[aria-label="add-year"]');
+ const removeBtn = page.locator('[aria-label="remove-year"]');
+ await expect(addBtn).toBeVisible({ timeout: 5000 });
+
+ // Starts at 1 year
+ await expect(page.locator('.years span').filter({ hasText: /1 year/ })).toBeVisible({
+ timeout: 3000
+ });
+
+ // Add → 2 years
+ await addBtn.click();
+ await expect(page.locator('.years span').filter({ hasText: /2 years/ })).toBeVisible({
+ timeout: 3000
+ });
+
+ // Remove → 1 year
+ await removeBtn.click();
+ await expect(page.locator('.years span').filter({ hasText: /1 year/ })).toBeVisible({
+ timeout: 3000
+ });
+ });
+
+ test('3.4 - Subdomain shows parent chip and FREE price', async ({ page }) => {
+ await page.goto('/register/sub.test.mpc', { waitUntil: 'networkidle' });
+
+ const domainTitle = page.locator('h4.domain-title');
+ await expect(domainTitle).toBeVisible({ timeout: 15000 });
+ await expect(domainTitle).toContainText('sub.test.mpc');
+
+ await expect(page.locator('.chip').filter({ hasText: 'test.mpc' })).toBeVisible({
+ timeout: 5000
+ });
+ await expect(page.locator('text=FREE')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('3.5 - Price breakdown section visible', async ({ page }) => {
+ const domainName = `e2efees${Date.now()}`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+
+ await expect(page.locator('[data-testid="price-breakdown-section"]')).toBeVisible({
+ timeout: 10000
+ });
+ });
+
+ test('3.6 - Registered domain redirects to domain page', async ({ page }) => {
+ await page.goto('/register/test.mpc', { waitUntil: 'networkidle' });
+
+ await page.waitForURL(/\/domain\/test\.mpc/, { timeout: 15000 });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+ });
+
+ test.describe('Authenticated', () => {
+ test('3.7 - No "Connect wallet" prompt when logged in', async ({ page }) => {
+ const domainName = `authtest${Date.now()}.mpc`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('text=Connect your wallet')).not.toBeVisible();
+ });
+
+ test('3.8 - Token select visible when wallet connected', async ({ page }) => {
+ const domainName = `authtoken${Date.now()}.mpc`;
+ await page.goto(`/register/${domainName}`, { waitUntil: 'networkidle' });
+ await expect(page.locator('.content.checkout')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('[data-testid="payment-token-select"]')).toBeVisible({
+ timeout: 10000
+ });
+ });
+
+ test('3.9 - Subdomain register button visible when logged in as parent owner', async ({
+ page
+ }) => {
+ const subdomain = `sub${Date.now()}.test.mpc`;
+ await page.goto(`/register/${subdomain}`, { waitUntil: 'networkidle' });
+
+ await expect(page.locator('h4.domain-title')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('button:has-text("Register")')).toBeVisible({ timeout: 10000 });
+ });
+ });
+});
diff --git a/tests/e2e/domain-renewal.spec.ts b/tests/e2e/domain-renewal.spec.ts
new file mode 100644
index 00000000..ab04d3e7
--- /dev/null
+++ b/tests/e2e/domain-renewal.spec.ts
@@ -0,0 +1,34 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+test.describe('Feature 5: Domain Renewal', () => {
+ test('5.1 - Page loads with heading, year selector, and go-back button', async ({ page }) => {
+ await page.goto('/domain/test.mpc/renew', { waitUntil: 'networkidle' });
+
+ await expect(page.locator('h2:has-text("Renew domain")')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator('[aria-label="add-year"]')).toBeVisible({ timeout: 5000 });
+ await expect(page.locator('[aria-label="remove-year"]')).toBeVisible({ timeout: 5000 });
+ await expect(page.locator('a:has-text("Go back"), button:has-text("Go back")')).toBeVisible({
+ timeout: 5000
+ });
+ });
+
+ test('5.2 - URL is correct', async ({ page }) => {
+ await page.goto('/domain/test.mpc/renew', { waitUntil: 'networkidle' });
+ await expect(page).toHaveURL(/\/domain\/test\.mpc\/renew/);
+ });
+
+ test('5.3 - Shows payment token and fees when logged in', async ({ page }) => {
+ await page.goto('/domain/test.mpc/renew', { waitUntil: 'networkidle' });
+ await expect(page.locator('h2:has-text("Renew domain")')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('[data-testid="payment-token-section"]')).toBeVisible({
+ timeout: 10000
+ });
+ await expect(page.locator('[data-testid="price-breakdown-section"]')).toBeVisible({
+ timeout: 10000
+ });
+ });
+});
diff --git a/tests/e2e/domain-search.spec.ts b/tests/e2e/domain-search.spec.ts
new file mode 100644
index 00000000..616711ee
--- /dev/null
+++ b/tests/e2e/domain-search.spec.ts
@@ -0,0 +1,48 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Feature 1: Domain Search & Validation', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await page.waitForLoadState('networkidle');
+ });
+
+ test('1.1 - Search for domain name on homepage', async ({ page }) => {
+ const searchInput = page.locator('input').first();
+ await expect(searchInput).toBeVisible();
+
+ await searchInput.click();
+ await page.keyboard.type('test', { delay: 50 });
+
+ // Wait for debounced search (400ms) + blockchain API response
+ // First request may be slow due to cold start (~10-20s)
+ const domainCard = page.locator('.domain-link').first();
+ await expect(domainCard).toBeVisible({ timeout: 30000 });
+ });
+
+ test('1.2 - Validate domain names before registration', async ({ page }) => {
+ const searchInput = page.locator('input').first();
+ await expect(searchInput).toBeVisible();
+
+ // Type invalid chars (spaces/special chars fail STD3 ASCII rules in tr46)
+ await searchInput.click();
+ await page.keyboard.type('test!@#', { delay: 50 });
+
+ // The input should be marked as invalid
+ const invalidField = page.locator('.mdc-text-field--invalid');
+ await expect(invalidField).toBeVisible({ timeout: 10000 });
+ });
+
+ test('1.3 - See domain availability status', async ({ page }) => {
+ const searchInput = page.locator('input').first();
+ await expect(searchInput).toBeVisible();
+
+ const randomDomain = `zzztest${Date.now()}`;
+ await searchInput.click();
+ await page.keyboard.type(randomDomain, { delay: 20 });
+
+ // Wait for either "Available" or "Registered" chip (blockchain response ~5s)
+ await expect(
+ page.locator('.chip.available, .chip.registered').first()
+ ).toBeVisible({ timeout: 15000 });
+ });
+});
diff --git a/tests/e2e/domain-transfer.spec.ts b/tests/e2e/domain-transfer.spec.ts
new file mode 100644
index 00000000..994eedb5
--- /dev/null
+++ b/tests/e2e/domain-transfer.spec.ts
@@ -0,0 +1,55 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+test.describe('Feature 6: Domain Transfer', () => {
+ test('6.1 - Page loads with heading, domain name, warnings, input, and go-back', async ({
+ page
+ }) => {
+ await page.goto('/domain/test.mpc/transfer', { waitUntil: 'networkidle' });
+
+ await expect(page.locator('h2:has-text("Transfer domain")')).toBeVisible({ timeout: 15000 });
+ await expect(page.locator('h4:has-text("test.mpc")')).toBeVisible({ timeout: 5000 });
+ await expect(page.locator('text=all transfers are irreversible')).toBeVisible({
+ timeout: 5000
+ });
+ await expect(page.locator('text=Verify the address is correct')).toBeVisible({
+ timeout: 5000
+ });
+ await expect(page.locator('input').first()).toBeVisible({ timeout: 5000 });
+ await expect(page.locator('a:has-text("Go back"), button:has-text("Go back")')).toBeVisible({
+ timeout: 5000
+ });
+ });
+
+ test('6.2 - URL is correct', async ({ page }) => {
+ await page.goto('/domain/test.mpc/transfer', { waitUntil: 'networkidle' });
+ await expect(page).toHaveURL(/\/domain\/test\.mpc\/transfer/);
+ });
+
+ test('6.3 - Transfer button hidden without wallet (ConnectionRequired)', async ({ page }) => {
+ await page.goto('/domain/test.mpc/transfer', { waitUntil: 'networkidle' });
+ await expect(page.locator('h2:has-text("Transfer domain")')).toBeVisible({ timeout: 15000 });
+
+ await expect(page.locator('button:has-text("Transfer domain")')).not.toBeVisible();
+ });
+
+ test('6.4 - Transfer button visible when logged in', async ({ page }) => {
+ await page.goto('/domain/test.mpc/transfer', { waitUntil: 'networkidle' });
+ await expect(page.locator('h2:has-text("Transfer domain")')).toBeVisible({ timeout: 15000 });
+
+ await loginOnCurrentPage(page);
+
+ await expect(page.locator('button:has-text("Transfer domain")')).toBeVisible({
+ timeout: 10000
+ });
+ });
+
+ test('6.5 - Invalid address shows validation error', async ({ page }) => {
+ await page.goto('/domain/test.mpc/transfer', { waitUntil: 'networkidle' });
+ await expect(page.locator('h2:has-text("Transfer domain")')).toBeVisible({ timeout: 15000 });
+
+ await page.locator('input').first().fill('not-a-valid-address');
+
+ await expect(page.locator('.mdc-text-field--invalid')).toBeVisible({ timeout: 5000 });
+ });
+});
diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts
new file mode 100644
index 00000000..7580029a
--- /dev/null
+++ b/tests/e2e/helpers.ts
@@ -0,0 +1,78 @@
+import { expect, type Page } from '@playwright/test';
+
+export const TEST_PRIVATE_KEY = 'df4642ef258f9aef2adb6c148590208b20387fb067f2c0907d6c85697c27928c';
+
+/**
+ * Login via the top-bar wallet connect menu's "Dev Private Key" input.
+ * The page must already be loaded. Works on any page with the top-bar.
+ *
+ * IMPORTANT: On pages with ConnectionRequired (e.g. /register, /domain/.../transfer),
+ * there are TWO "Connect" buttons — one in the header (top navbar) and one in the page
+ * body. Each has its own SMUI Menu anchor. We MUST click the header one specifically.
+ *
+ * Strategy: Target the button via the SMUI TopAppBar action-item CSS class
+ * (.mdc-top-app-bar__action-item) which only exists on the header button.
+ * This is more reliable than text matching since the button text changes
+ * after login (from "Connect Wallet" to "0033...8f2c").
+ */
+export async function loginOnCurrentPage(page: Page) {
+ // CI flakiness: the SvelteKit dev server can return 500 on cold start or be slow to hydrate.
+ // Wait for the page to be fully loaded and not showing an error page.
+ // If we see a 500 error page, wait and reload.
+ const errorIndicator = page.locator('text=Internal Error');
+ if (await errorIndicator.isVisible({ timeout: 1000 }).catch(() => false)) {
+ await page.waitForTimeout(2000);
+ await page.reload({ waitUntil: 'networkidle' });
+ }
+
+ // The header button has .mdc-top-app-bar__action-item — unique to the TopAppBar.
+ // The ConnectionRequired body button does NOT have this class.
+ const connectBtn = page.locator('button.mdc-top-app-bar__action-item');
+ await expect(connectBtn).toBeVisible({ timeout: 15000 });
+
+ // Wait for SvelteKit hydration — the SMUI button needs JS to handle click events.
+ // Without this, clicking the button does nothing (no menu opens).
+ await page.waitForTimeout(2000);
+
+ // Wait for any loading spinners to disappear (page still fetching data)
+ await page.waitForLoadState('networkidle');
+
+ // Click the Connect button.
+ await connectBtn.click();
+
+ // Wait for the dev-key-input to appear and be visible.
+ // The SMUI Menu opens anchored to the header div — the dev-key-input appears inside it.
+ // On testnet, the dev-key section is always rendered when the menu is open.
+ const keyInput = page.locator('.dev-key-input');
+ await expect(keyInput).toBeVisible({ timeout: 15000 });
+ await keyInput.fill(TEST_PRIVATE_KEY);
+
+ // Click the Connect button next to the input (inside the menu, not the header button)
+ const devConnectBtn = page.locator('.dev-key-connect').first();
+ await expect(devConnectBtn).toBeEnabled({ timeout: 5000 });
+ await devConnectBtn.click();
+
+ // Verify wallet is connected — the header button text changes to a short address
+ await expect(connectBtn).toContainText('...', { timeout: 10000 });
+}
+
+/**
+ * Login via wallet connect menu at homepage, then navigate via SPA link click.
+ * Use when you need wallet state preserved across navigation.
+ */
+export async function loginAtHome(page: Page) {
+ await page.goto('/', { waitUntil: 'networkidle' });
+ await loginOnCurrentPage(page);
+}
+
+/**
+ * SPA-navigate to a path by evaluating goto() in the browser.
+ * This preserves Svelte stores (unlike page.goto which does a full reload).
+ */
+export async function spaNavigate(page: Page, path: string) {
+ await page.evaluate(async (p) => {
+ const { goto } = await import('$app/navigation');
+ await goto(p);
+ }, path);
+ await page.waitForLoadState('networkidle');
+}
diff --git a/tests/e2e/profile.spec.ts b/tests/e2e/profile.spec.ts
new file mode 100644
index 00000000..29c3caed
--- /dev/null
+++ b/tests/e2e/profile.spec.ts
@@ -0,0 +1,62 @@
+import { test, expect } from '@playwright/test';
+import { loginOnCurrentPage } from './helpers';
+
+test.describe('Feature 8: User Profile', () => {
+ test('8.1 - Disconnected: shows connect message, no table/search/pagination', async ({
+ page
+ }) => {
+ await page.goto('/profile', { waitUntil: 'networkidle' });
+
+ await expect(page.locator('text=Connect your wallet to see your domains')).toBeVisible({
+ timeout: 10000
+ });
+
+ // None of the authenticated UI should render
+ await expect(page.locator('h4.domains')).not.toBeVisible();
+ await expect(page.locator('.chip')).not.toBeVisible();
+ await expect(page.locator('.search-bar')).not.toBeVisible();
+ await expect(page.locator('table[aria-label="Domain list"]')).not.toBeVisible();
+ await expect(page.locator('.mdc-data-table__pagination')).not.toBeVisible();
+ });
+
+ test.describe('Authenticated', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/profile', { waitUntil: 'networkidle' });
+ await loginOnCurrentPage(page);
+ });
+
+ test('8.2 - Shows address chip and "Domains" heading', async ({ page }) => {
+ await expect(page.locator('.chip').first()).toBeVisible({ timeout: 10000 });
+
+ const domainsHeading = page.locator('h4.domains');
+ await expect(domainsHeading).toBeVisible({ timeout: 10000 });
+ await expect(domainsHeading).toHaveText('Domains');
+ });
+
+ test('8.3 - Domains table contains test.mpc', async ({ page }) => {
+ const domainsTable = page.locator('table[aria-label="Domain list"]');
+ await expect(domainsTable).toBeVisible({ timeout: 15000 });
+
+ await expect(domainsTable.locator('a', { hasText: 'test.mpc' })).toBeVisible({
+ timeout: 10000
+ });
+ });
+
+ test('8.4 - Search bar visible', async ({ page }) => {
+ await expect(page.locator('h4.domains')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('.search-bar')).toBeVisible({ timeout: 5000 });
+ });
+
+ test('8.5 - Domain link navigates to domain page', async ({ page }) => {
+ const domainsTable = page.locator('table[aria-label="Domain list"]');
+ await expect(domainsTable).toBeVisible({ timeout: 15000 });
+
+ const testDomainLink = domainsTable.locator('a', { hasText: 'test.mpc' });
+ await expect(testDomainLink).toBeVisible({ timeout: 10000 });
+ await testDomainLink.click();
+
+ await page.waitForURL(/\/domain\/test\.mpc/, { timeout: 10000 });
+ await expect(page.locator('h5.domain')).toBeVisible({ timeout: 15000 });
+ });
+ });
+});
diff --git a/tests/e2e/tld.spec.ts b/tests/e2e/tld.spec.ts
new file mode 100644
index 00000000..0d8a7991
--- /dev/null
+++ b/tests/e2e/tld.spec.ts
@@ -0,0 +1,33 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('TLD Page', () => {
+ test('TLD page loads and shows .mpc domain card', async ({ page }) => {
+ await page.goto('/tld', { waitUntil: 'networkidle' });
+
+ // Domain card must render
+ const domainCard = page.locator('.domain-container');
+ await expect(domainCard).toBeVisible({ timeout: 15000 });
+
+ // Avatar must render
+ const avatar = page.locator('.avatar svg').first();
+ await expect(avatar).toBeVisible({ timeout: 5000 });
+ });
+
+ test('TLD page shows Whois with contract owner', async ({ page }) => {
+ await page.goto('/tld', { waitUntil: 'networkidle' });
+ await expect(page.locator('.domain-container')).toBeVisible({ timeout: 15000 });
+
+ // Whois section must show owner (contract address)
+ const ownerChip = page.getByRole('button', { name: /Owner 0/i });
+ await expect(ownerChip).toBeVisible({ timeout: 10000 });
+ });
+
+ test('TLD page does NOT show settings tab (isTld=true)', async ({ page }) => {
+ await page.goto('/tld', { waitUntil: 'networkidle' });
+ await expect(page.locator('.domain-container')).toBeVisible({ timeout: 15000 });
+
+ // Settings tab must NOT exist for TLD
+ const settingsTab = page.locator('button:has-text("settings")');
+ await expect(settingsTab).not.toBeVisible();
+ });
+});
diff --git a/tests/test.ts b/tests/test.ts
deleted file mode 100644
index 5816be41..00000000
--- a/tests/test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-test('index page has expected h1', async ({ page }) => {
- await page.goto('/');
- await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
-});
diff --git a/vite.config.ts b/vite.config.ts
index e01b77dd..a9322d64 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -18,13 +18,6 @@ export default defineConfig({
}),
tsconfigPaths()
],
- css: {
- preprocessorOptions: {
- sass: {
- includePaths: ['./node_modules']
- }
- }
- },
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}