From 8f8a873cd195e66152421ff4e47d3cda1c949cd2 Mon Sep 17 00:00:00 2001 From: Caesar Kabalan Date: Tue, 15 Oct 2024 00:21:18 -0700 Subject: [PATCH] Add Playwright UI Tests --- src/.gitignore | 5 + src/package-lock.json | 84 +++++++++ src/package.json | 29 +-- src/playwright.config.ts | 99 ++++++++++ src/tests/.gitignore | 2 + src/tests/deep-functional.spec.ts | 192 ++++++++++++++++++++ src/tests/default-homepage.spec.ts | 22 +++ src/tests/import-export.spec.ts | 47 +++++ src/tests/real-world-functional.spec.ts.gpg | Bin 0 -> 25342 bytes src/tests/subnet-basic.spec.ts | 174 ++++++++++++++++++ src/tests/ui-error-handling.spec.ts | 76 ++++++++ src/tests/ui-usage.spec.ts | 168 +++++++++++++++++ src/tests/url-sharing.spec.ts | 134 ++++++++++++++ 13 files changed, 1020 insertions(+), 12 deletions(-) create mode 100644 src/.gitignore create mode 100644 src/playwright.config.ts create mode 100644 src/tests/.gitignore create mode 100644 src/tests/deep-functional.spec.ts create mode 100644 src/tests/default-homepage.spec.ts create mode 100644 src/tests/import-export.spec.ts create mode 100644 src/tests/real-world-functional.spec.ts.gpg create mode 100644 src/tests/subnet-basic.spec.ts create mode 100644 src/tests/ui-error-handling.spec.ts create mode 100644 src/tests/ui-usage.spec.ts create mode 100644 src/tests/url-sharing.spec.ts diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..68c5d18 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/src/package-lock.json b/src/package-lock.json index e83b5c8..6ffaa2f 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -9,6 +9,26 @@ "bootstrap": "^5.3.3", "http-server": "^14.1.1", "lz-string": "^1.5.0" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "@types/node": "^22.7.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" } }, "node_modules/@popperjs/core": { @@ -22,6 +42,16 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -212,6 +242,21 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -458,6 +503,38 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/playwright": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -558,6 +635,13 @@ "node": ">=8" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", diff --git a/src/package.json b/src/package.json index a3b579a..1c59230 100644 --- a/src/package.json +++ b/src/package.json @@ -1,14 +1,19 @@ { - "dependencies": { - "bootstrap": "^5.3.3", - "http-server": "^14.1.1", - "lz-string": "^1.5.0" - }, - "scripts": { - "postinstall": "sudo npm install -g sass", - "build": "sass --style compressed scss/custom.scss:../dist/css/bootstrap.min.css && cp node_modules/lz-string/libs/lz-string.min.js ../dist/js/lz-string.min.js", - "setup:certs": "mkdir -p certs; mkcert -cert-file certs/cert.pem -key-file certs/cert.key localhost 127.0.0.1", - "start": "node node_modules/http-server/bin/http-server ../dist -c-1", - "local-secure-start": "node node_modules/http-server/bin/http-server ../dist -c-1 -C certs/cert.pem -K certs/cert.key -S -p 8443" - } + "dependencies": { + "bootstrap": "^5.3.3", + "http-server": "^14.1.1", + "lz-string": "^1.5.0" + }, + "scripts": { + "postinstall": "sudo npm install -g sass", + "build": "sass --style compressed scss/custom.scss:../dist/css/bootstrap.min.css && cp node_modules/lz-string/libs/lz-string.min.js ../dist/js/lz-string.min.js", + "test": "npx playwright test", + "setup:certs": "mkdir -p certs; mkcert -cert-file certs/cert.pem -key-file certs/cert.key localhost 127.0.0.1", + "start": "node node_modules/http-server/bin/http-server ../dist -c-1", + "local-secure-start": "node node_modules/http-server/bin/http-server ../dist -c-1 -C certs/cert.pem -K certs/cert.key -S -p 8443" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "@types/node": "^22.7.5" + } } diff --git a/src/playwright.config.ts b/src/playwright.config.ts new file mode 100644 index 0000000..c7446da --- /dev/null +++ b/src/playwright.config.ts @@ -0,0 +1,99 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + permissions: ['clipboard-read', 'clipboard-write'], + viewport: { + width: 1920, + height: 1080, + }, + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + viewport: { + width: 1920, + height: 1080, + }, + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + permissions: ['clipboard-read'], + viewport: { + width: 1920, + height: 1080, + }, + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/tests/.gitignore b/src/tests/.gitignore new file mode 100644 index 0000000..6bfb121 --- /dev/null +++ b/src/tests/.gitignore @@ -0,0 +1,2 @@ +# This file contains a private dataset used for testing real world use cases +real-world-functional.spec.ts diff --git a/src/tests/deep-functional.spec.ts b/src/tests/deep-functional.spec.ts new file mode 100644 index 0000000..e42b300 --- /dev/null +++ b/src/tests/deep-functional.spec.ts @@ -0,0 +1,192 @@ +import { test, expect } from '@playwright/test'; + +async function getClipboardText(page) { + return page.evaluate(async () => { + return await navigator.clipboard.readText(); + }); +} + +test('Deep Functional Test', async ({ page }) => { + // The goal of this test is to identify any weird interdependencies or issues that may arise + // from doing a variety of actions on one page load. It's meant to emulate a complex human + // user interaction often with steps that don't make sense. + // This does a little of everything: + // - Manual Network Input + // - Subnet splitting/joining + // - Colors + // - Sharable URLs + // - AWS/Azure Mode + // - Import Reddit Example Config + // - Change Network Size + await page.goto('https://127.0.0.1:8443/'); + // Change 10.0.0.0/8 -> 172.16.0.0/12 + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').press('Shift+Home'); + await page.getByLabel('Network Address').fill('172.16.0.0'); + await page.getByLabel('Network Size').fill('12'); + await page.getByRole('button', { name: 'Go' }).click(); + // Do a bunch of splitting + await page.getByText('/12', { exact: true }).click(); + await page.getByLabel('172.24.0.0/13', { exact: true }).getByText('/13', { exact: true }).click(); + await page.getByLabel('172.24.0.0/14', { exact: true }).getByText('/14', { exact: true }).click(); + await page.getByLabel('172.26.0.0/15', { exact: true }).getByText('/15', { exact: true }).click(); + await page.getByLabel('172.26.0.0/16', { exact: true }).getByText('/16', { exact: true }).click(); + await page.getByRole('cell', { name: '/15 Split' }).click(); + await page.getByRole('cell', { name: '172.24.0.0/16 Split' }).click(); + await page.getByRole('cell', { name: '172.25.0.0/16 Split' }).click(); + await page.getByRole('cell', { name: '/14 Split' }).click(); + await page.getByRole('cell', { name: '172.30.0.0/15 Split' }).click(); + await page.getByRole('cell', { name: '172.31.0.0/16 Split' }).click(); + await page.getByLabel('172.31.128.0/17', { exact: true }).getByText('/17', { exact: true }).click(); + await page.getByLabel('172.31.192.0/18', { exact: true }).getByText('/18', { exact: true }).click(); + await page.getByLabel('172.31.224.0/19', { exact: true }).getByText('/19', { exact: true }).click(); + await page.getByLabel('172.31.192.0/19', { exact: true }).getByText('/19', { exact: true }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/20 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/20 Note' }).fill('Test A'); + await page.getByRole('textbox', { name: '172.31.224.0/20 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.224.0/20 Note' }).fill('Test B'); + await page.getByRole('cell', { name: '172.31.240.0/20 Split' }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/21 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/21 Note' }).fill('Test A - 1'); + await page.getByRole('cell', { name: '172.31.248.0/21 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.248.0/21 Note' }).fill('Test A - 2'); + await page.getByLabel('172.31.248.0/21', { exact: true }).getByText('/21', { exact: true }).click(); + await page.getByLabel('172.31.252.0/22', { exact: true }).getByText('/22', { exact: true }).click(); + await page.getByRole('cell', { name: '/21 Split' }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/22 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.240.0/22 Note' }).fill('Test A - 1A'); + await page.getByRole('textbox', { name: '172.31.244.0/22 Note' }).click(); + await page.getByRole('textbox', { name: '172.31.244.0/22 Note' }).fill('Test A - 1B'); + // Join a subnet + await page.getByLabel('172.31.240.0/21 Join').click(); + // Change some colors and do some more splitting + await page.getByText('Change Colors »').click(); + await page.getByLabel('Color 4').click(); + await page.getByRole('cell', { name: '172.26.128.0/17 Usable IPs' }).click(); + await page.getByText('« Stop Changing Colors').click(); + await page.getByRole('cell', { name: '172.26.128.0/17 Split' }).click(); + await page.getByRole('cell', { name: '172.26.192.0/18 Split' }).click(); + await page.getByText('Change Colors »').click(); + await page.getByLabel('Color 8').click(); + await page.getByRole('cell', { name: '172.26.128.0/18 Usable IPs' }).click(); + await page.getByText('« Stop Changing Colors').click(); + // Make sure we're still not changing colors + await page.getByRole('cell', { name: '172.26.128.0/18 Split' }).click(); + // Check a bunch of specific items + await expect(page.getByLabel('Network Address')).toHaveValue('172.16.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('12'); + await expect(page.getByRole('textbox', { name: '172.31.254.0/23 Note' })).toHaveValue('Test A - 2'); + await expect(page.getByRole('textbox', { name: '172.31.252.0/23 Note' })).toHaveValue('Test A - 2'); + await expect(page.getByRole('textbox', { name: '/22 Note' })).toHaveValue('Test A - 2'); + await expect(page.getByRole('textbox', { name: '/21 Note' })).toBeEmpty(); + await expect(page.getByRole('textbox', { name: '172.31.224.0/20 Note' })).toHaveValue('Test B'); + await expect(page.getByLabel('172.16.0.0/13', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/13'); + await expect(page.getByLabel('172.24.0.0/17', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/17'); + await expect(page.getByLabel('172.26.128.0/19', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/19'); + await expect(page.getByLabel('172.27.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16'); + await expect(page.getByLabel('172.28.0.0/15', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/15'); + await expect(page.getByLabel('172.30.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16'); + await expect(page.getByLabel('172.31.0.0/17', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/17'); + await expect(page.getByLabel('172.31.128.0/18', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/18'); + await expect(page.getByLabel('172.31.192.0/20', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/20'); + await expect(page.getByLabel('172.31.240.0/21', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/21'); + await expect(page.getByLabel('172.31.248.0/22', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/22'); + await expect(page.getByLabel('172.31.252.0/23', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/23'); + await expect(page.getByLabel('/12 Join')).toContainText('/12'); + await expect(page.getByLabel('/13 Join')).toContainText('/13'); + await expect(page.getByLabel('172.26.128.0/17 Join')).toContainText('/17'); + await expect(page.getByLabel('172.31.128.0/17 Join')).toContainText('/17'); + await expect(page.getByLabel('172.31.192.0/19 Join')).toContainText('/19'); + await expect(page.getByLabel('172.31.224.0/19 Join')).toContainText('/19'); + await expect(page.getByLabel('/21 Join')).toContainText('/21'); + await expect(page.getByLabel('/22 Join')).toContainText('/22'); + await expect(page.getByRole('row', { name: '172.26.128.0/19' })).toHaveCSS('background-color', 'rgb(255, 198, 255)'); + await expect(page.getByRole('row', { name: '172.26.160.0/19' })).toHaveCSS('background-color', 'rgb(255, 198, 255)'); + await expect(page.getByRole('row', { name: '172.26.192.0/19' })).toHaveCSS('background-color', 'rgb(202, 255, 191)'); + await expect(page.getByRole('row', { name: '172.26.224.0/19' })).toHaveCSS('background-color', 'rgb(202, 255, 191)'); + // Check the Shareable URL + await page.getByText('Copy Shareable URL').click(); + let clipboardUrl = await getClipboardText(page); + expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAzlKIQMY0iEAmNAvomq5KDAKaMALADNGADgDmjfAAt2nDHJ5sOIAJxSe6MUuCq0a3StUBWUVrSFNvQkcQw0ukIJgBLcYIBWjBtADEwsJ0eIEgqmIm3lq+IAFBIaIqiIIAzO5aYnhRoDF+dACGgUiJiGIY2SC5BUWJxpxo1nUgKQJaIfIgGOagaIKNnCbWzbYdKY6MeG4deGnSMFmMMCYwANYdSylryvow5YsmglugAHaoACp8lAAuAAQAQmH2JiZHICaWADaMp9AIlaiPN5oNBfCyEGAwAC233Ol1uAEEbgBaG5wfTglLQrQwQiCPA-E6w643REotH2XEYAkgH4gC7E0mosLGFmsthAA'); + // Check the Export + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "base_network": "172.16.0.0/12",\n "subnets": {\n "172.16.0.0/12": {\n "172.16.0.0/13": {},\n "172.24.0.0/13": {\n "172.24.0.0/14": {\n "172.24.0.0/15": {\n "172.24.0.0/16": {\n "172.24.0.0/17": {},\n "172.24.128.0/17": {}\n },\n "172.25.0.0/16": {\n "172.25.0.0/17": {},\n "172.25.128.0/17": {}\n }\n },\n "172.26.0.0/15": {\n "172.26.0.0/16": {\n "172.26.0.0/17": {},\n "172.26.128.0/17": {\n "172.26.128.0/18": {\n "172.26.128.0/19": {\n "_color": "#ffc6ff"\n },\n "172.26.160.0/19": {\n "_color": "#ffc6ff"\n }\n },\n "172.26.192.0/18": {\n "172.26.192.0/19": {\n "_color": "#caffbf"\n },\n "172.26.224.0/19": {\n "_color": "#caffbf"\n }\n }\n }\n },\n "172.27.0.0/16": {}\n }\n },\n "172.28.0.0/14": {\n "172.28.0.0/15": {},\n "172.30.0.0/15": {\n "172.30.0.0/16": {},\n "172.31.0.0/16": {\n "172.31.0.0/17": {},\n "172.31.128.0/17": {\n "172.31.128.0/18": {},\n "172.31.192.0/18": {\n "172.31.192.0/19": {\n "172.31.192.0/20": {},\n "172.31.208.0/20": {}\n },\n "172.31.224.0/19": {\n "172.31.224.0/20": {\n "_note": "Test B"\n },\n "172.31.240.0/20": {\n "172.31.240.0/21": {\n "_note": "",\n "_color": ""\n },\n "172.31.248.0/21": {\n "172.31.248.0/22": {\n "_note": "Test A - 2"\n },\n "172.31.252.0/22": {\n "172.31.252.0/23": {\n "_note": "Test A - 2"\n },\n "172.31.254.0/23": {\n "_note": "Test A - 2"\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}'); + await page.getByLabel('Import/Export', { exact: true }).getByText('Close').click(); + // Set to AWS Mode + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + // Check AWS Mode Settings + await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Usable IPs (AWS)')).toContainText('172.31.254.4 - 172.31.255.254'); + await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Hosts')).toContainText('507'); + await page.getByText('Copy Shareable URL').click(); + clipboardUrl = await getClipboardText(page); + expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAtqgIIDqAygiAM5SiEDG7IhAJuwC+iNAMigYAUx4AWAGY8AHAHMe+ABZCRGTeMHCQATlXj0i3cANpDF-QYCsC02kImJhW4hhoLIGTABLJRkAKx5uaABiOTlOPBiQA0V7MNMIkGjY+IV9RBkAZiDTRTxU0HTIzgBDGKQcxEUMMpAK6tqcuxE0N06QfOlTeK0QDCdQNBkekXs3Po9h-J8ePEDhvEK1GFKeGHsYAGth3fzDvSsYJp37GVPQADtUABVJFgAXAAIAIUSve3tbkD2FwAGx4D2gzHSP0BaDQoOchBgMGopnBIGeb3eNHeAFp3nArIj8ij3DI8OD7k8Xh9sXiCV5CDIMBSQGiMTTcfjEnYebzBEA'); + // Set to Azure Mode + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Azure' }).click(); + // Check Azure Mode Settings + await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Usable IPs (Azure)')).toContainText('172.31.254.4 - 172.31.255.254'); + await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Hosts')).toContainText('507'); + await page.getByText('Copy Shareable URL').click(); + clipboardUrl = await getClipboardText(page); + expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAtqgIIBaAqgEoCiCIAzlKIQMbchCAE24BfRGhGRQMAKYCALADMBADgDmA-AAsxEjLumjxIAJybp6VYeAm0pm8ZMBWFZbSELMwo8Qw0NiAKMACWagoAVgL80ADESkq8eAkgJqrOUZYxIPGJySrGiAoAzGGWqniZoNmxvACGCUgFiKoYVSA19Y0FThJoXr0gxfKWyXogGG6gaAoDEs5eQz7jxQECeKHjeKVaMJUCMM4wANbjh8WnRnYwbQfOCpegAHaoACqyHAAuAAQAQql+ZzOR4gZweAA2Ahe0HY2QBoLQaEh7kIMBg1Es0JA7y+3xo3wAtN84HZUcUMd4FHhoc83h8fviiSS-IQFBgaSAsTiGYTiaknALBaIgA'); + // Import Default Reddit Config + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await page.getByLabel('Import/Export Content').click(); + await page.getByLabel('Import/Export Content').press('ControlOrMeta+a'); + await page.getByLabel('Import/Export Content').fill('{\n "config_version": "2",\n "base_network": "10.0.0.0/20",\n "subnets": {\n "10.0.0.0/20": {\n "10.0.0.0/21": {\n "10.0.0.0/22": {\n "10.0.0.0/23": {\n "10.0.0.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n },\n "10.0.1.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n }\n },\n "10.0.2.0/23": {\n "10.0.2.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n },\n "10.0.3.0/24": {\n "_note": "Data Center - Physical Servers",\n "_color": "#a0c4ff"\n }\n }\n },\n "10.0.4.0/22": {\n "10.0.4.0/23": {\n "_note": "Building A - Wifi",\n "_color": "#ffd6a5"\n },\n "10.0.6.0/23": {\n "_note": "Building A - LAN",\n "_color": "#ffd6a5"\n }\n }\n },\n "10.0.8.0/21": {\n "10.0.8.0/22": {\n "10.0.8.0/23": {\n "10.0.8.0/24": {\n "_note": "Building A - Printers",\n "_color": "#ffd6a5"\n },\n "10.0.9.0/24": {\n "_note": "Building A - Voice",\n "_color": "#ffd6a5"\n }\n },\n "10.0.10.0/23": {\n "_note": "Building B - Wifi",\n "_color": "#fdffb6"\n }\n },\n "10.0.12.0/22": {\n "10.0.12.0/23": {\n "_note": "Building B - LAN",\n "_color": "#fdffb6"\n },\n "10.0.14.0/23": {\n "10.0.14.0/24": {\n "_note": "Building B - Printers",\n "_color": "#fdffb6"\n },\n "10.0.15.0/24": {\n "_note": "Building B - Voice",\n "_color": "#fdffb6"\n }\n }\n }\n }\n }\n }\n}'); + await page.getByRole('button', { name: 'Import' }).click(); + // Do all the Reddit Default Checks + await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('20'); + await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24'); + await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24'); + await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24'); + await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24'); + await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23'); + await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23'); + await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24'); + await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24'); + await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23'); + await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23'); + await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24'); + await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24'); + await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers'); + await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice'); + await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice'); + await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)'); + await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + // Now change the whole network address + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').press('ControlOrMeta+a'); + await page.getByLabel('Network Address').fill('192.168.0.0'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24'); +}); + + + +//test('Test', async ({ page }) => { +// await page.goto('https://127.0.0.1:8443/'); +//}); diff --git a/src/tests/default-homepage.spec.ts b/src/tests/default-homepage.spec.ts new file mode 100644 index 0000000..16efed4 --- /dev/null +++ b/src/tests/default-homepage.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; + +test('Default Homepage Rendering', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page).toHaveTitle(/Visual Subnet Calculator/); + await expect(page.getByRole('heading')).toContainText('Visual Subnet Calculator'); + await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('16'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/16'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Range of Addresses')).toContainText('10.0.0.0 - 10.0.255.255'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Hosts')).toContainText('65534'); + await expect(page.getByRole('textbox', { name: '10.0.0.0/16 Note' })).toBeEmpty(); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16'); + // This "default no color" check could maybe be improved. May not be reliable cross-browser. + await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »'); + await expect(page.locator('#copy_url')).toContainText('Copy Shareable URL'); +}); + + diff --git a/src/tests/import-export.spec.ts b/src/tests/import-export.spec.ts new file mode 100644 index 0000000..9075747 --- /dev/null +++ b/src/tests/import-export.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; + +test('Default Export Content', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await expect(page.locator('#importExportModalLabel')).toContainText('Import/Export'); + await expect(page.getByLabel('Import/Export', { exact: true })).toContainText('Close'); + await expect(page.locator('#importBtn')).toContainText('Import'); + await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}'); +}); + +test('Default (AWS) Export Content', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "operating_mode": "AWS",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}'); +}); + +test('Default (Azure) Export Content', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Azure' }).click(); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "operating_mode": "AZURE",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}'); + await page.getByLabel('Import/Export', { exact: true }).getByText('Close').click(); +}); + +test('Import 192.168.0.0/24', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Import / Export' }).click(); + await page.getByLabel('Import/Export Content').click(); + await page.getByLabel('Import/Export Content').fill('{\n "config_version": "2",\n "base_network": "192.168.0.0/24",\n "subnets": {\n "192.168.0.0/24": {}\n }\n}'); + await page.getByRole('button', { name: 'Import' }).click(); + await expect(page.getByLabel('Network Address')).toHaveValue('192.168.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('24'); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24'); +}); + +//test('Test', async ({ page }) => { +// await page.goto('https://127.0.0.1:8443/'); +//}); + diff --git a/src/tests/real-world-functional.spec.ts.gpg b/src/tests/real-world-functional.spec.ts.gpg new file mode 100644 index 0000000000000000000000000000000000000000..171724030d56e50838bfe08947e84e2fab149c73 GIT binary patch literal 25342 zcmV(lK=i+aUISvF>{#%!1&$H{2S5n&lp?WCfzNmrbuuWYTk!|3XoZcVE#KDuQg(9Vv(zM__kaoNLBTOH!G?5I!63tBhR?0U??2$XvM;fxy32l)cGU`hD{=5HY#=wfT-WR+d|4T3JCNQNK9z z0w6t`t5MiHQE~Q5&ZtEMhiqq5R4Ll(vU?%Skeb=z_NP*qMl1~Aa}g*k1W1tG60Wmb z{H;q^uX=iwFv#PA!#6H%opouuRZ6&ND^aD)6sW^9AhJ~qyOze&>$U?H3s}J;bLcJ8 zvMLs_(TjRpy&!4NHijZ~+>u2in#@wYsFFF>^}V3q$F7i}tdlOXLO5IBaW9{#c%s5R zvzmpziM5GCdu(HSX1D;$O*N%X zhq>yXF@GI{dh@$907XY%>=VN!H;y!Ate;npn3?iA=>+a7u)`?f$(b3tM+q%OKFkGA zld*96nIRb*zOmk+eGPMxOV7L4A$*QLyJBFdjWUhe@uoTQ-Hd#1bh(eOfr~f-c{e0! zP==%hmi{rz5RiTe^7N+dAZmeeYet}brgp|ImP9&H9!M;gA~NX@>*6)KZa0kZ9p;R- z6=A449ljZ7z=QBkQP9E|e>YGnZE>LWaHC5D8>z$KKDmgvRSjQ)J0zejltn__S!usb ziF2?fe2ouCEb$Y6eU&HmwCAPS^PQwpS9=Wq6uaaJq{BC#WQk7uqI;D6^ue|@CGb;i zmn%rV(G1;nSC%As7x0!atZNqZi_3RjdO0bevI8NeKa1xnk6CyKbQG}-+NQn_Hy;1< zLG`AC*8`XR_jF_PjSvI-Zo2vP=#eam6`5jI{2!P(3H;nEpS;Ov%v?lQF+ z7Io(eW9PHdXRZSp_BQK18Wbm~7$Awx z6M8adJiJQo$6S9HbWs$T?5VPht@g#}@T1@eqertRca_jo&J4_Th|CK`XjID5v-t<-CD<4qv}AG1v0a%%8FC;m4-l_<2Q(Bt7#zr!f!^j6;D92iG_=P382nm#!HX_g?A zj@l7#SjH@JcjQkpnO_HFq|R=RG;=U(Cd`7c6Pbw*1Q=M~cow@>(c8glqse4@cIUzj z>#ZOzTlmSg_r1zkS>RO0gg~|)(#;W}Nuu|*;v^$w*f8j#7O8YzFHxN2t*q$fUHqq< z6f$sM>U6hhgoS@r^p9c0+h9l@dHuDyYl1-js0@QJ?6{A&QpsS9%kSE4+B`(CxK1z8 zKK3Cvf_;DO7kchj(n7Th^6@5|FSk(rL25h#cx?VZB$U9mxz5arc}?o2lIW6JvBqi#2?&x0jyq){p$0mIw{= zYD_Q0k9z3qd9xzMNvznOu({7zz9VQJLyLm6ddF~{P_+trI>*svJOM=4ilwm!pygR* z93nOh$|5V}5=pQ^{pTRU6c?YOUu+}Y(%z2~ZuRFCmGqsgOX5R0ppoF}7%V7odGVWJ zn;epp@{r=XZ$b%CbQ1j)=gVIx;zYYssK)5ozS!1W=$7=wiiBn4hcjysO&y~s4XQTp z?2zxDOxzeQern*Qy(4xGO)t#?s$L_Tk=5oUYg@B@tv4Ks&EoKK%{d&%F%|{z4-qId znyQ425K24{+=96vnjj3_zbqKgB_tb9b+eweJ!nKR8e3C20WWk{2gp)Sl(;8W)#J1; zSnKSSBj#aD;a@S+gR(GRq?v>-qEkoY4P}Eyvezah%AkV>(iS}k-UcES!F*mz-%`OX zP8^ozZeZvB#2o61$#AHzUauz$3$(!E@i?8|Hbm~`8d6)va})npQg9f%DxF^k1pWp{ znBc3(DFF;DwQ8j{=rYL370b4wdVd?kFwA8GoLgPm??@j&ttl1Fg(nMdWa4wN8LT}q z(V7Q1^+>YgTa(Ij+u&sWa5ujp4O~@c1L>sVXMj0BwXPTlj8fYcEppxp9;tA`UL&Eo91W^{hp-5LkG)={?QBTNK!d#UeDDB?2z64i8$O0uLvDjGH6YrcpI^4 z4Mi1Nj=(f;?Z@E07`MR|{siL}0*E0_`sBTM-~KM?OfDAb|6bxGt+BmHtucbIMp-2u zo*ya1@urTZOk9JP#fO@H=K7c@N%?U`lE`I3q0BRl$1$y|Bi>INen-s+@AB{!`Lg4A zwJ*yvC=NRpQ-X3^O%8(2(Hb_oPzrVJ1|A?(16(u|g`@Kt2#uHsmaCujQD4+6{Jb$% z)VY`mt=+7(Cxx6#ynhj!VEa3hGb8^Y{Dp^$uVb&=0&8UJJDZdEdX+9Ey(*?*t1}S< zSAZYZGb(h%y8HuT)ud^mjF=um>OZ$ss=19wz#=M7{r`sAk;>cISRX?Z?PzW2IU&|C z#7|F?fsdhgZ}HBv7e6KpxXEY7G2a{dmw(BY1*^-T)Y&{46Zs4dXv4>t^A2*?+Tg6U z`~FGfx5Cxaqz#hXzHI`}qhaA$R;U92O->sw{(K;Aq+Q;h^mrJIEe@*=8`PNIgUDCO zLV)xEarj;tW$J7m|G@91UHiUZucrKNT>-RY!^m3W*v!ohg!X_NAx zDo8TI545l47e8Dt)<<+g5F(<)bA*ziN!0@6c>EEP~)l z4f-BhK83C>QqDJ!2mr3bTN^c1xv)GL0iJHMWSO9&rR_{QGxf$9-S+d|w(M*^i@cdK zO~G%PH8h~tFb3+4>$>kfT>kQa=!aXC%71le3vUU3etXb=VJ|+5s*RM~woPp4W^}KS zvcn+OpS*X+*t(~S9CHL28Nth4`{<3nKI~$c)EXAF8(FWhUdY(%ubxqZ=oPQd-~f?Q z+UE6eLIEkWS?bF(rnc!Nr`f8yn8+w|GW^FEeaB_@niWruTcc7sxAdhophG~ii6o9( z@Fb(vXYKwvNel&${||A&CMwsH-ca-^Diyn~hpkY>#F6Ka%g%Ca()ylF!wB zP7Fh!$NC4f+%TTCKT&A+Fr~eGd(6*ktK`Qgm@005GwaHrs@MF;xzd0SdisN$k(-$? z^r-q9NmUI5^KR@gpF~hCA1vOeyzheg$a0;%2Jk~bwNro_>hT!l(4`RvzNl^K(Yj=X z>Gw|VcyPi%UK>sHeqRd*+pasC{>Axb9?nNxuQ@qmMcnJNx-3tvz7$JVvZ}O7&xmoX z$Oj_ePGGl`p}5&H21LNd&4Qa=bJa@ll%RObabgYpLZZ-1K-r=Z?931Z>T_V^tPre% zesWmdD=*JzJOXgkg6K+9&&#;X`<*ax#Ut9f;os>5ywniUc+Uk;1Y_1!XE|E0>7wh~GW>jWRn{+nZq-lPH^lSRC zFA|y-_B`s5a#%hX=O`m_pa_JAS~Ts><==bp*{VzS-K3<{s0m3Boc9*$5R`~9LFy!3 zu7jA?PZ616HXr?+>S(n$xE!QM{mU1Q;@N~WU&XF-LTu#z< zi4}(H%+`(~P{%}8jo@y$dONv&eP-EH4!&oy??azxdp}h9yp5G%XaTBLs5cX-JWL>f z*YbLvQ{J&V5YGe(3n7}t9e8f;Fx8jE8MCeZ$ueE!Ub-vX)=7ZwDKSgcV`P)?g;St15PxeFs1qiLv9y)?4U6;n0Yt ziOLJIx)ArlQor`JaCq!ABNUNc3qg;(15EeX7th=E(AEFN{q-Xd5D)3C%zT4B8;fwQ zJcv&RH{biF@xH!Bx4*i7Yv@cqtj4+&sqxy*_=VYQWZN+ghNo%tQCTy@UyJZV=m_ZC z3l<{vInTZ(!c!eZDi7x{s{R(z|IK^(e=K?TTeHu+ApOGA5ipTP;L@EL`a4tSDX^I? zf+D3Shr`7;aD=kj5OCdG2cC^&MwW#4gQ0oha0}nah2KRK@o=j<^vg!XYj@`7Es{P0 zP^kR+PDpuSO#L9DYatoQ41AWHSh=XF6HLVZVdJoohH5kfoRdBY4YuI-SXK$c6yP|J zHeqfiy{fp>Jzl@=gtAtT703d~f}OjD>eI^lqV4BY8;v{npb>9;wYYXAm&*q1#C`fw zrsV5#@ZLWts8;buX|LHTevmlA)E!p62{NTzjZo@Nf*3MU)Vg<>Tf{;`QG2>i1E4v^k=s?PehCw?Yoe zA1IRqqL;&T(bWO+fKQ$lit2TM*^9@!xcHzHo%xM3=n!trB+a$_j;tIp?UP;qDPf8%aFU zc(@xOIuu+Vw{vl*gRB@VOeH;~Uknj2=PoKBU6TlYBe3UR2b)8ItRl7pxx|7rjv|?!aZ5zTH)25zGS=e_Az!FBQ%^ zmJ7~JeDxMxI*V~q_VCWrc?R7{gpcD8No$B^!08>H(epsp28tz4X(DaJj#;Cw`pt7i zSAhJT#_X~Sl+l;;{8ti@OT$(_;F>2i{0m?%!neks%28VMK8J6g_35-ZA-S9s!bVUq z^>N762Ttn_3fvNoQn|{-T-)=q8MSL@Z`i>LiJk3F}lbgBP7rpgW1|62N{M{Y+m4c5P#Ao#~}F1Twb58<0k=fB`(G zgWO_IJ*1^PHBc6{#HnrW%H6w@&{ z%-XEO#3$ERe3Ii#Z>`esvF{%zh!mfA?`tY`OSsu_UDn8EKEX>xHmI1|=BM!DbO)Mr zCpdk|IJR$e>K$!%VG5TQR!TPm?M}*l9=6GjA_je7hYERpm58stp?BE_*VSnj4AK%` zUszU!(&dJ!oLTAX?{0}Q(+ki?bOP%+bkX9J1d8X8OAcPn^J6+H`lm(%cP7V@r8;>$ z^)F9oiJJ3Z%$8@d0X+x_3Vd@&@6Ksw-&S6huA-lOzqK#_4>Ns{<{!up7ct=4sBrY6 zCdYrx)749-Z`0@(^Gs(6Tw>(e;5kwX%+`QGdS0bF!CB4xI&lwh%22dBvEnEOZ96$m zRnwbL$6Sm7ICCgbKYSqlPy_!z(mwRkj;k^l>s}ry zi98=pS;a@gkalQZv%A>&m~5Yr+*=0vHo*{nj-R7%W;%nK#&#PL)$-y5c=H5Ze9Y|E zcT$K$BZ?93?QulZt~JQBB&y7B}&U=~J@RXvQcY@VfcQ;31$QSGU**g{CjGFqzo-!vJ`=KRrU`eqf(q!7`O;GmH3 zxbTiGcKs?MqVI?r5RM^}YpG0|H4Jz61o8#|!g=^`ilGFf_juCc#jsbmkR_B!e}%69 z7WpQ|vpDMa6$be;KRKhtJiw9XpIy6x1D4S8y`DT=(o{L-=G^J90n>V=g?OpU z-f=D*_uxQW{=NCO7B#Kc@Q2ggQ@%8UB!Z%6G}HoPic!rVF^S!c8vX|i7MQGH0v3f^ z^P_kxF7G(zIyYaDoWQPUL8u#9)3J?L;atiJGt7;8yyy^h=Uu50>@(0k5CoPWicvP%5Sd&oMk82mO6c*O|7sC`2>()Iqasv+f63iB9_kD^Up@lT z4ob7#t;4#XJxRF$r^aDe12HfM_c;X|(5zHKDUiR`nXXykx|0!$7h04Gdy?+phVj#m zP~1@s!*X96ikA=Tk^h!j&IY2|e{taj(8rR+9`w^jihJSrwS1*CtINUAcuwO-wr z5lDv!+xjKH4+W7jly#niW~ARU?C3>;4!#h~=^I$YosVdX8@qGA3Rwrz86hZ@6IH*t%2d@Hd)lM7-@f-6T zN>yFZsnXeU=2`jrIP+e}0o!PI0=_;780Dq4Nu-N_$akArI06o}ix)6J66=H{r#Rmi z);pIoNjXzv^R7tqeShnd(=~ZkN&^#M@V!u1aJk+*;`H5)N_1PLjccuOVOwid)`Oa> zSi=Wf7bI~y_x)OeBv5Vwk=DE|D>GSXwvG4Q)4RTpCIcW5h;DhONTSA?dCMLy6Bq)5 zBq^B>jQKR#X1x);UtT1fRrl8vQfy3!YFS)?fjzJ>35wh-Mf>RClb@FPHq>xP-7_1Q zNhUZ^9)VJI6Nd(KfMe7{OC`Ci{#Zh!|D9#HUWc zA(@{*GW_@gum#jMWgr~k-C4gvq3lKj_9so(59B?d$ zLPt^4z#FMSvV|g(I~|r-xWWb7CI-!rY=U`sWEAS~j>ew~uYvGZ)G%JLr6t4(m&a!) zcvO~ql3BfhXHn=*!3I3F^EZVCTn_9iqMf53eM;!)g!bvG>)Afwy=BPPNP=ki+d{+q z?`1~^*FB+DN=XzozxtGTG{kH_p(F^Vz%&}3(cygv<`s-20s_8zN6pW+Q`nkXL+t?_Mx0F1cZX=9NE4K3}OvWG?! z>CNR2H!0o)vdIJ-31Cg0_6v1LZiZ|nuy3x`oo1LWd{Z=EAvk6SsAfU|Wdq1rKo+Aq za0=A9dU!{pmfU{oWpA^(L?yq7f2F{ages1NvvpX&1j?;NWkz?+;Qo02oQj}{TGc6x_x zweS)uPrXWgJyaD{*lJ8Hu)6Dp1Cgqx5`lFr{@XnLF8AueF)MB?L1c&Ja0VCB8Ebad zHCLpWy3jFB8r1gX^2;6S^vqu~#T`48mcEelaoT#^)yH`K0kY!;Fkz;2Zz zD+GWgY)&O#H#ri>w&ViwS)tW=6>&f(k~7oE=)NF@IYenb7P0|2PSUO*v`YO3u)`&D zA*~e;;qtyg_#HmVxPx-$4T!nk6BPy(3q-4L9{#n)_ydCVQ}NKL)uDtHESx)hpBUwLv9vIqxD6yW| znkeKF<>kN_i?}iOVH#&@c$MHrts7y&4rL#5F@#L1%YXZ(Y3moBjmoPmiKv3Fx3mCV zrBFgp@|Gw+;J%+P*=sH!w&eVuYfyz1Mnxt!3vLiNra)W+XCwjj(v%CR7~(9s7*L0y zyzI@%3hE6h^&C^-G&&2ivI$lpgS9N%C4Ef*%nAn0Dzj$JOYxGujfU$R>8?W!>rHca zqRx$6z!6wnKFEJ?)@fs#AVyc|Jk_|RS?KfI=SgIu(n!DVsxk`wqK`uE!7MhLe@(`x zqP_-ej}$~~IUesJK<~1FHT3OU+jpgwlj(bcEB5xQt`~&9(fT03yhqsbsA*gsn^L(@ zz=A6gv_uDq(MYm|zGq0bnjc7(3$$ANHt&PkJ+-NAz#K~m z!sat7PJTE_@;nLW(W>RVfx0|WoTb{I8)NNXG@}e@R1jQyoQ|G#)gs-54`AOS6FGv z6|om^u`7HlgD#X7n7MrO6>HY_o5ytUbjIrsKT2EHsyx=24bf^~PCVAo`5;g~TcOMY zlffgyn|AywuOLJELJ90PB>drWS6QRBw<~1tV>yQPV}d9O=WD#0%Ye#KX&_teSZ|LIweo%uCxrYQM`wEp)Kn=<-&e_*;I%s zlPOkXPC(>3LEf|Ujq(=>5k8`bP=RJ=H@x64H%RTG>ae-JDBk&e*i@;7E@5%Tv~-FF zA;#A?h0=(m0n0ek98=qU(yFRJ54tki-;@{XWqR- zH)cpx;~NRsO?AYv*GlLWU@*8$hQfbt@Ih)U5*F4CkX^h=00|2`FI>(pErx^}so)2n zPuAbSDpd}os|&%`HPnCk|hDeIU*d8gdlWSFQD%Wop-3ijv-`pRQ%IY z&sJ9d-&aLE>ix{kW*CBdM@Ev8 zzOOQVY~hvL;)F5qz7Kj}QcNW7>J=0urRQ-8o%X|Ga%1mN)CRBFl%0W!JrPPEa1=!x zP+=|SNos$a%rxZAj03{^&4a$FM=`TaqSIAi1{X;NOjod8`ncT96)}pqu2<~~Lcx&q z3F0#hK^2$RufC|)$G^y=_e&39f>O3Up429056>;m64C|XvpsFkr63d<|38c{s}d8+ z!cly;$(R#k3LM>0w(M;UvKzyEs!i&Uf7;}S@^;0Ry|%FM)&EIt!tZE$hSp%x@cc_2 zqNjE2we{yTfWG=3T3K~#lZ2v>_5H_d+AmUxUt8*uZX(@a=-M^e(Y3QD2&uTY4Mi>j zp5-HuXkEl2~Y(=b88%k{3L7#cr5t$Bji^hUul0uU4nF=`t=q$K>3 zM$uU3$7i})G0cwe9)#{YBiH)5HS*%G)b#;%^fEmlh5dC9oIoE4-#;8W3)a9_N2(WG zkaCLs82BzbY(p%(>=?yRtkg(d&^IH`D-9&_XoUM3qL$T3o*{&$;+{uN)``#!Q0M$+ zb0~A$CjD^zh*Bzze+zf<{R)48ahJ-`_U*(!@}gXemUIcZnxW?SL&M}gGQ)GDiPOi^r!971oDX_l>ZFDtuQ zk5wqI3TlYofutOT)ohpgLiG^< za2h2HIgF8LK|g5yJ@n3|275cjiM`6Hr7=*TLYw)wXiCWbjb3ib`_2m)p3e3Df7buw zWNrpmF>q5zgF<3|u)#K(d&j1lxkLlL=gC#;Ip`*-vy^wg@mU%-NwiT}wPm;isg_1# z4|&9FB7zb)^M?IZ=!T{iNTO*K%^AgQ=kya2(?8KyGfE)uwRyu-^v(nvD z);4W!{4}xdDtG9~{JR(kq$=g3cE1q+P%*02MT7d=_#Q)uUhvYW&04uUCxgfc%4@gn z<*9#SH@Bzx4Xi%D(RcWPTt|C?latI{)Yz8)Is^tK5(f{JxTDK%O#FSJWIgdK@sU$7Wo zNRt_2PS2(>4r4sP7MXgh2_*d6=8as7s5Ma=f1rH46>2gk*Q79#LyHPzW_Uj8w9=QiTk_}O?K?v~MTZ?l=i!SgsiHeRBbM%Ln^vVUY zJ&?$}%zZxKaB7!%2NrA7l&QH$jDTBd%&(n48$0vSCV~RbJTv%lv~CpXP1ZYLprHal z`#7>iE)teXO{CrAIF+gh+6Svsva=lIV|fQq3y=GP5PeYK%OGVy6#~T8bw-X|JM*5S zv5g5$;OD%@jY!?-_uylr?YfS+%DduJSZxXo^CHb=lSJo?`-6a2uX1n_SgG&VnHP4T z!Ix>7UshH{U#M6%gVouM|AWa`fR!g!XTWy`JK?ENm_M7rg2*Z!|)8TldOVPsg41NJ2moA z%x&#vVd?kwZ}v!95)HE3FRd^P6je|1nGpsVjt`@a8;KhwCbcfTlZ8%Y(9WXx8Ga5Y zc`c&pL(1DSBLk(#S<9n`*)h@t4W@f}b{&w&xM*J3uo8$^ZwFDl!yRq#pETxs`_XIA z9Glgs4c=K3B8Y9i+K9tpLWU2>gP?{_^;dRcM0%5_mZ-CWwoB0CQ@!Q|vGkN@-FvtA z{Wj~hsqn2xFmHBwor$iX|Aq0#iIWdnvJFd$4v4Kj4Svn=S22=ro1dw*AVD9CRt|58 z^KTQ@&6=mW^SsQz90R`NB@*;BcltBruazpsJHO9^I@YDta3dYNwr~Lni zGqfE;l$A(Z4JGR1%*f5;O2-q}nIy*JY*~wBORou; zU2+X$#bU!2$@5AY?{dfKHp)zH#!v|?wup-MZM*_5bgg#KPI{VUU$D@D7|PNYXFEdU zIsMc4o{zM`Cx`K4Kr>KOQYz;7`uE1A#{qDDI#SGCN%H>g*IyQDOCf9cX9=hVuk+!m zK(S4QK}kronQH|qM1n4zA&0ksNlYyN4Wx+b31M=tG=*uBnJn-b&{gwJ^;Pwtoi|kJ zQ~_u|czERtfApT@2krL>;t=4VLqeG8k(TOgxJ)}bl(X68pXX6$?_1Qj@~>$6G1Ua%7O5=`_AHiHngrw)9Qxor?`!BI7%_n`x`u4 zDHOWkffvu{eL#(KF=BJZ2kLo=U8?Wach%`iM;f&(x_R!LGUwG+ZomyfPoN@sJ+qH^ zRbM~j1SY;`^_j1-SZsNJR>p@{tx4&R|(E{njzXv+N5 zrMmQSij~1L2gtTBR*9({81l>D!$nj=tRCPpoeWSLot$7V=k+(Otpx;;4_7xC4}*#~ zI3dAEJ8l61mQ{)!$s7*Hc}Lgj4pcg$=~^YAobvGdI3z9J;{vL)@^) zfKf`q$+k%J|0TkHOMzY$k}F*isiS?y%i3odMS2`v9vuXWot(5Yr#)9Wfv*lsr#cXME@Lg2uQkg|kW4O+~uP8Up!F6~rjIAV)Gxv1WznHW?buxuPR1oB8XViUI7qHE!KuuI4n( zPuT#RX8`%0htRn8?91Du0?j*p^L<^`OH$3^X*ZKSsVzHv=Vy4OCdTklk6A?_V46;- zC{MpyaSgeXr`l7`J~0zYn^Qeh$>1y`x;WbQ-53(v(PLn% z`@+#PwX9#vq@(9k)-EF7rL9ps(Q5_R->2>`w6_aw%^){wI0PI? z1GF{;2d+vjUQB&u!^e3>6=3l=;~ziMe`^OANZr1PG$0vLY^)=)pV<-X=dImntA9ex zZH@7SoU)|xjB0;L6>p>F`KSc`FkUx-Oy8uU;PklfP{6||2iBIzc%*SmhuY0q6;h*6 zt`Ui{c@`=`Kd)2CnS09LhBMdhpWLrry>MU2bT@d>&2Ccf5aC@q$Lzxq=wWmPc#g;4 zTN5@2%2zY2Q?|T8h_M_5{}zTayww@*atgR-fr9C+PR*Y7`Fit{K*?O!yzbNf$Io-) zOC9)vnEW~NZoD!-Hq8zazwEa0S=1`>UY4A~ilU7VmSDTkhJERqeRy&|eu4F6`+d@j zGxr3ZNOGO!kpKK@NcOoX_@9_gy1ctKhM~~)rT-Df^o~IcEZ|N3F_Cn}jhSL`(BGe? z>#UH9p4M?(bI7r(QErZvWvZ{;*eZO09jT8TMWjEY_#EiPp40r?`L?taWlgo;UUZRe z6izOTZZGZ0(Y(0zWZ-X>i~Rq|6E1{oV*u{AM=thYO}{J9it}So?k;=3l48zzCA-Is zw%J-E=a@{OVXM&Wqk`XcX-csl+%@^or~GNmEcaxWe8IF?bA!v=JVptD;Cx0yZzRmjYPoKr{n`Kjt7*Ja3 z33Xo+Ko2Y(o~Mg9N5m7;(d`t{+E#?Ny%9udyyKF^<%lOeb*yduf($(+bX#g*p!1s| zJUBFNQAWxKCO(b0A(DtDVy!b#%D+3E_y8i2v}0=>qYbbW{sND|(exU7apcJ;=xITW1D_)VXNi2D(WxeUN|C^g^;% z4|NTAIAY)CvNd87TzdunX!^r&o1;2gdnP!SMuN|P`qQQ#O?C4tpScDwETL!iJi+KP zO{Ww!361r4(oT*M0f7yuT`D8msloTWebwNZvFA4>942kanfK8UA7$7Mj&$&mXSo@7 z!q((wk^J+98oB>~v{Dh{QDX_7Ld2*Xx~r0_!ZVw%n+es=M3|5A%K{p4o7afZ@dHo7jjfQSuZ#E%_R9LzwuDzsuF{GH%c z41QJ3bTb>pt!M*@s#3RKaQU?cx}zK+cwpM3cN5Eu(o6E1W6d7jqAaCImcIhh0mAa* zqP|<9RHHBpzW!qRrSMz8G_wLvz1&W!al0qr6xo29??X3t+i0b_e~4N`L2I!WIJg2f zcB(tkKJnOhRu&PsHS}B9%B=3CVQR)5mlcp5v;ZSSLwPMfi=oyme+}H6>+aE{-+SYl zAyO6Q;rB%Uvi4E2yQCssDz>tunS!x23g)*UubM^%NntWEvC?Ls`e*gZ9+d>pdge$! zsuxB5ZQ>SSyR&cDBs4H?4A?%o3qh_b7rMEaTEV95JbjDXSbk^HEJKneIi`k}ySo7t zc4wFEk6w)#c^OISzdbtpdXCiZjtnet*vhK1NH0l`YVc$4lO7_%Sqoqs1>Tev1!zPa zT(8;z1FXNIJxenR?3AhIzAovwImv0<1pEY`nbTJoLJCfjHpj32N}Y?r86TU5$9+jSBrgBAl|HqBAA9{Bxz14sq5C)rC@>ylh||f1L@H^r zpuP0Vcc~`DX1cwtQ|fpzsA^!_Yh1R7qHG1Qzxwg5h25qGu?AHiKMHE@FvmG272<>S zf#LY49@lD)=_Dx1<<3f%o#7w{RL$L^VpoO*cRb7C=bCkoyXJ-J+gW|1bAG~wvszAB zxEIp7=2BKhVxP$z&z$767@dIUBnPu}nQ|g5=$5J_r91e&sDWVz&OJ)wImnF+!jXDz6K`(-c47Ff9`+iRPw}oSu=?R{Wes&t1UaNiKrr~YTcR3N% zRPO5_3A9S##1o^Czc|lE{*VJtsIJ_{uNl|-?tKBvQm1emvYv;jv19bALs#`qzv zr1cZxuq9wrccMTYtWV%#**QB7>q++kJ+?#4{yRj~kVbj=nWqTB+|_^!3E+)83yc8~ z?r|I&o{7pb^}vk~kwv1^8&qn_gOChT_3_mQHN~ z=x*5bh#LndZJhP_$|h~B=1YY|Ls8Nr5f~Hoe=OecIg4~XcsrqZtrty`(B%X$m68{M~zD{L~F|#aulztUU z>i?f$C@mKpDmo_oS?NE_T`WW254cV4<^N)i|4JpasZ5CTyu~9TQ=c7h2q$;(D;+&J zmEUHUeNDc^Q&pAX#-;+{1wBe1;(CKvcSJ<+5SK}oX{PiwdzS&@e%bpAe9=MoQ@+5O zAhEV7>vK`j6#ZEhA_6nykzdcmW-7VMHNQPcG(HQXT{Rf*>>BR>Dy@<- zXb^LMk1+8iZ*Qp+A#adZO1lUD7V-M(i z%rx>3u@0PXo~vMLlFyBOI>}y9`hgg)wBDgJ+k5Tw`No2R>Z|}u+3rUkp#P6Tj6zHs z5O&SJzJ9}=5@YMk=tBPyCI0UPCT0pqizP}bcxEU=h+Q_5I{}kX5zzqnegR_-cZLQo zgoc1Oq7o{cQpUwaJu-!$pRz?31UA<69LVU;xk6cDN6vNUlJq~Fz^g}|wCI;<>!O{m zyT;p3QA2M*<>`VPTzA8Tb5f1G>Z|I~6X zMUl<{ZuukJr-gdxKNCZP!r?ek@X;#K9*INay-!YUgm1tv0GqMnkS}KkuYpIkx#XYQ zyLOo3h2r)(4bxGt64D1a;-TI4Q$x`58ThLx+8n?~prDBI0Tw8FB2HH-wQ^oz=&?I{ zIqxODovQ;|qSICX%_H_3M&o`1l$3|-+i4KajRu-Lj+@d zD{(Psj{(&7-EnA-!9i)s zPn6ovP)qP<=zzpa8}%{qEdflH{MMV3`ybvF39i3skqL$_-me7-d$sn7WKZ~+0#7mu~93x=-MXIT+7qVuRS=c zjhExSiG5jpKT*_zO01EcRJor)U_By6!|&DPi6tneIDQqP$m5X4=GN6^p^O4y^tJR~ z4y7>KTi@xQYbU|JsZzCKL+!7#j9Pm@CUlD_79bn(tbAE#(gcesEfBuzJOcS#%H=55 zNJ!uimbAV`lM89tKf?4cJWqZyx(TWJZBHR~7@1#cdTEs`8|jLKnplp8`h;V&E#MKkdS> zLJr?hci5BJ-jWq1_9!$R9Dy5lq9G!<(3+J|2FQ21mn|O6{84#!K1kwVMDD3lS$$g} zlveg@55QoJ_HuHCO48Fm##XH|aCH93mGikF0Kn7D@!*hzMGWp;gY2QLyGV z4=_!gt0J9y(*@PBODpKT@_K>Blt$H-whpFIu-(@H@nh*lA%~omE1Z|!VfchLa;(ku zsTRFl-`hbG?$6CJL(ta1CMN>@23FZAFrhCTHq|{4k}cwlhI)$J*Xp3W2ee6zfBLQXHMv7 z8dFx5*%Uw|QPdufN?#E`7|Ah_G?U3ES`-a$oJ5DmoDI2(K>=nX_OGUmVL63M8C5zg z$5(a}Ixh57ohqr|v@FzGVkHNv_%qCcpxQQ&NR6!J8?}Wio~gC^lCY5%Y)>eEghhp` z*u?s$&+TSG40)+E(RQl~J5QWY7<9Q-21K|`I<|TF^zqZ7BPaB&vmAI!IZ(3 zm;S_no47#~Bb@KtoAz*cnKbcbfNKEK7eb%6FXe5llL_VrGbx?po`~)j)|dVU2!H!> zXUc#37;~_aEuGv#R%a$9#jq5=5Ljbxb4y>tUB!rlcvP69U8i-{}O50suVy-m3wVDrd%!IfB<0&6; z6mILNN7Jvb?NwC~4CRY;mqy1kbMpN7|JoKZ)03~eKxo#YiNL0)V=eL%Q#;qqjJ}-z zoA88l2FB?ymHCQd4|_Qil^{P$zEDlvnA*$stm)8d%@K=0ha2J4ZCHXv|LNG3`luHB zSo0)@J~M%)9LGPR1wn5;T0iI-#v;B0PjFxfOGe@W)aY+{Q%5+P_~K$(M8%$$C&#<6 zR(MM@@Zp_|=z6(b#bKrh#%N>DE#-~sr5|rd#b_YRV8k%w?HbZIe!fX zg2nQg&BZn~{N*zx5JHjs!^LNMGo+;npt05bP&yP4fMdLYHeXmz_LJ)Fr)JcaOWFb) z)!`M34YMM09Jg}R^x*2|<$o&D_5r3~P`gy@jps+n&RRvqV|Gw-nasfml?(*gn9i`u z&kHE)RZdxgx$@)j<(8s1t&wg^yz=a&Uk|ZandTTBh-!zEqx&;ta8%ZDNykz))#2S9 zOcoi)_G?E#i+N&|Ws~H|NI+V?B@5H|8OPQ~;ZtBgk>j|>jm4*TFRM#-Tc9-uDV0a) zCI{xYWXfRaN;{SR8;u@eLZ0iVYw;vtHbY?k=g2#9BK2WSEnf@bpA~%AZCLX4Q1n}- z4*^GP@6wx4dYTqB#u$~VZdpM^N`x-Qy7^HKOq3l%UDz+i9fc9y}R^*E1UOW0C>$A`IQ;CG*4O>}RcS-J4WZNMr)R*8hlHOwqj@ zzLiHKXv`L>ptF*9bWHqwMUal_tvWGVN@+hn>Wn38>9< zzQ87B?d092a^6wAbJ<$_L>0x71@DI*(gR>ql~7f?RPIf5e(-)AUH`zet#>#@o3n7a z6#6V!vKCF;#>fo51OguKk&IxPW#+%Lvn1i02oV@_ev`Yd|Ek@IxXi8|*d6v9{Ozv- z4TUON9J-vdPC8w1Kwb(&3L;{g0J;dE#R6+b8z8IR_MK_WkeOX*LgFCiyx;H`=!j{g z$4(K1i?7X=Sbr!-Azy{{3i8^u_0`@Hg;mc^8wCAj|9C})pvD~(W1g!#{l+qojs^^6;@4W(SK^HBHgM>T--w*_qg7lek zVi_`r%&JOK7lOxRu{KNy{|0gDtFw7>$^ZnheDJUw1+C;#7PQIp(mG0&(mSj@1g zg0Zf#G7s%q5Hve*QWCwBsQOwI>LvffCQWj*TDA{ZS~WSRq--1WZh*4LYYiP%*16CT zv3L>GRhuVTmfvc8`Q=>Y&&^m%^uzc*B((^PO0>4*7W#BbhtHW790da(YwX8QNUZMY z>~7%Eom(2;&t+gN9^GG*@h~DtzsaegN5D_v z2!28AZ_Ch3sN7RgtGH!r1m&ftT6F1RY-i;HFDII4%zqK5O0L+wKOw(Q+ZG0>&cVGH z-}<(scHr#SY>Yo2( z8{z)!rF^6um(4h4UO{A1=gz^iBu9i-nP#W*8hC6@p3Zo%bvIY^RGz7;Q@wls+e;j0 zj&jwoVWmx-n-2+swrMnq8OP^5M4@6#>DI8G2@Kgg{F7j~joica690JU3|VmuCBk&Zw=fTVS~#=t!@_`Wr=g zas4n+Vp&1qQ7Aq-c&g0nKi5ngJ!4*Q*{y)|b>dwsSh%EvlnPk2W?kF1$<r1S|$)BTEs1X~SxWb6_$~=C=?vj?Tbx6uP6xf?0 zOk;Uayi+4jx|%t{S)p+h#}Ik_WgiVca0(;YBDr0oCL#?r7<4n74AWm#Q&A#%mCu*^ zWU#_e3`c1j%MHK2y1kzSp;{BTN-UPjc>|Mbxd&KeVn>EXVp2b&`LNrqZji}xq=k+3 z^v}2hUza7%>#J%SNsEFLWG{GK;0ys)8_E6KI!kYo3zJHK5mOtg(_i7Ai6!#j^@o1M zSn`U;JmFAQ`MLgOw~C!Tzx49juRLdX&4X%E6)O}Ojnz7C#frac>5D_=$uvwM*dpxKL43fA6oJy*0 z?<|%;aRaEFJ8tmgsH(Tl$<|<1XDKt6-j&nr=}a3FE=bA;Z9N_17GwFzvpMr<6_EpQ z=S*4W=ZEbf(j_Ci8Dlv34BMs)0FO98a!b2Um4K$@xe*FGxN_sw?lM{4a_U&Ic-2r+ z1!K_FDSZNBG{;PVx0K(C8!^`1A$UXwxI~lztfmfXu%pS_%DK7;u53CL4cW7-{b#plrgIdyLLr|E5(axC& z#9G87w}_S7U)IR@E%C-_RPRo0~$B z0?zOosUlTw4X(jOIH*>(Bq7QMe^Mj=EkC3S)z06HMR!)8(!>sI-6C;Wyu77`WbwyB z>h`U@FWCDW^8cA|TH0A<8n=cS=r;`uq{X{UzpJ|n%j^Uy><;i!8Zxc-#?QZzl~GF7 z=LS0yo&+C|(b(0aT#Vxb>Y`$Ft?UkW4VS%W^mbudBuUwda@qi`63_o4DK{|4ssQCw zyv7Ruh>*P59VLXN(nxqzDjE-C(pFRWwke`R&iZ+e(Haw`T{Rw{nHcF92CRES&JFu~ z60kDE^pVTKH>RPv%b(Yg5U`9!mB9ovD7!h@dv^0R_7ZH&h0hM^&tLg4m>xbNJh)$< z(ygEizslT=sAsVfvUtMyuJL9QGgh8RoLs4I+hYEbrSc5vb)p{8FO z`KFpK3b1PUTUh+JOxH7dO}tX~vpM1B&yBx_@QV1*UWZQnTvBrO=S>dvZd{v3D}2=~ z$mBnizW;GJ8B5lFueT>nF~*D~M!Ile;m3y)nH0QwUrdjAXU8eIkIH3jhCuJxR*TsZ zCMM_zuQF#n_14g3KqZ;)#!pz;6Gg_no+1U&8Q=j5Fgfm28IYSch@^mN7~nDWt;Mjw zS$p7T`=V%{c^Y@;e&N`KxJKvce*6_j4Txg)Sg-n64R!L*Tlu6>eU9?t`)6(TL+>1D z!*MpWIdn@@;kVA<+-gIr20W&@Oo%Fo8~R3^vCcoA0jp59q^|tnAr?4!XDUjYF%P<& z0cUm1<;IbXX!1sc2peR0T}Os56hNzekG6f?=#{?3lXXOpPfN^5X)hfszV>~3T}L%l z1n8UZ;jsp(-FX6y7+9lk^1X-J%xbD4D?i|%L15bcm+%t!ZvTomVgJ!(ck!_0%g9sm zE_8J6CvIWWCy!7zd*C(;apDA{9@-m3VOwy9;5etT9HdJ}WjaJ{CXe2FGhL2I^_7V{ zQX2=zaZ@h~>}&?x*x?Dy*Xt!$-BEKOOyj>L%dKcGsLqDDFBmSzf4t-L)OBQ?RLh{W z^2NKFOj}}2cAVo%#Q(b>-Q=+bfbLAe)K3RaBTTw2DDvhK*z`$K15GZITfH;is9p+2)NN#k=euEXOf2*qMSs|ka(gly!HZ#5u#!DPjE@R4 zY*=L|qdc2APc%q^V}V1$(3lw=QzvA40jN&=X1x3~db5jHah;nU40l16J!QS)E}&s7 zGnepOIm&Y}{^63k;{6py51;&d)(hIpvr*~!UKM&Si7;JMqe#nxRdM2`=C@M${tM02RTn@@ z%kw|TZ0sUD2x-!TZElD+c(L|4i`~~!zT)wODC=aV!dZ%+7FG$J_cijPEV%{20FIDy@HSL0L17fU~){Eeav-5Fb9b6CEn8 zi4}SkDY?Owp$Du4#5Fxg?+6^B1?;gzPRdET?E${bX%Mn@XS$Lf?wIp2Jyd1D;F9*< z?4At2I`7SL^7vS!X1ZyHAB$v+05i{e2PjNG$E|>&juYil@FuR2OOvy=b zTByK8=YWV77)!3MbioeE`c%0KCMnBMb7X+a6I%dN$2f4&i~|kwc!_Qi)GDm2Y}+Z; za7SB*lB3Kh!-K!5i4!A9E$_Fmw__!LxbPg8RKl`H^~{)@jOL0gOy^8$7&E>v%F5a zn!0RuHL3avBC{eX=^2tjVh2B}z4SX|$r+1Wv{!!RjcaB=$P z9rY6wVZ$?*+tzC#?M46yxV$Z2`07{LQB&quF6_NIGc(6$##vOFGSF~-mHM>fQ=Ho) z#VaqygJ%EIb_q*YJ%hK1D_~(-mh}yLtccb;MHy$kXCq{hP-QQ1D9a6=GI|beRJ*mtn--; z<6PG1-m=XHL&v&o648RicSl zIQC4*G)eOt#?dHMLD4l9^6x~hY(JcA!FT7o^Wm(sou7tBWfb85wWQ1YhOym%K^%my z)pl*WZk*Bk{jm_Ss)ZxCxeDjyzOqTaTl}Ghj-&Ue&OV?ME9S+?5eBu0$Z5z3n?<64 ze*Ubq2X#g=!5vY4VBwBIKO(^7MYF5H{n3cvBmGYl5%{G65qh3k*C2D`L+|0a2N!dT z$UNfd+)|5L;9r!>Yob^r#!~h@^1x>gH6_qs%(6o2cD7;%GaW3SVq92LA+&p0cwPsa zVG)X{_&w$zJaTrGRKP0cxK@P)CW8sz#70m*nF|#fr9BSM+3NWbviC(ia~W+<6?1=3 z`KBfhi0@5YXO5{A+9=v6!}@|5Ra-A2lmN*4_4bH1dgcFf1l%ZT@FCB#`B-;NBeKij z_T}o3sDv^Mw6j&O9N%MWcGPd-v)Zln8E|xP7Lgin5|Xq_gT}qAIaPN_yN5P%IBah4 z^GUS5&{gbVMyWz9AKvt2v1-)LCyfaTF|= z&|m!em=iJqbX{-%sIgga1yThGFT2l`L;I-%h08(zN$eb`l<)~v#6sQeBIQ3JCy9c3IVQ}wI35;7ybzRxOS+ah5|0Y+~ zH566{NVC(5xftghpTB>~yTz^4V!E#tywsCPi|;B;y!3XY4{wF7wI7(1z(BccG58$} zRaO*JFVUTu;AQ~kU2a7SnIlp6dWeFYqa=n4sCUtlElxDm5;uSc_wZ^Pm0#drC^nvM zUKq-0x+5f$JxxxGxEnaY8_d^er7J7~y-q9~Sm+i1g#IunVFM7bH>vTwjxZJmQ12BJzA& zy*6rnDQ1t(w5XsFT03WcgR%GLC}va+Am+hz-q|pfO5zGCs02BH!2|Y7R``7lQcw76 znFH6^jK>nz7&$2T9RZwb-*v#59tIP8!=ZWLwaPD+1I$R{j1kcKp`9%jbJ`$X*eM>Z z);h&N^J$>_iCJYe&r0MUKXPD{XMkmh03x9P?JN*JSxS7mhqQ3+>rd|S-?;7@mO!HK z12g{I5oLu4Ck}l=Y)6fSZQUoR2ae+_&3@Yj1>N>ihb5hlO#oBQhQ%L&XMCDE9*T*N zF@o8Du(O0af}Rg}0DqaJb#-1lk#w)!iR)q2iNW`k;n`uFGAn*w+Hp?d!D;mP>UxU} zSuqzuA1^q)S$By`l*I0s#nR$i zAp}dgEyb6SGq8RsCMif7DXy?U3LTIlS~oYXZqVO;%0j>h17+WO729ezQ7qj!6+J)) znC2>$2$ei3b3HzPk?xA3!x@j#C0aE|zTQs$WiIEyEHnU(E`qeTY1wRe*4RTl=)z?| z%m2qEoUQJV&~(V{iFn~vKV}N#k(rai@M{-5g}JY4dV|lFl_|SSs<94bgK z)UR@{%79-Vh#$X}a*YW4wvcUiOd-xTRjQ)Zg~)01=ODCKSThfOYFrF_&O=>22{ZREEFhMdR?_{a;rCdJtc-sYX zj2pt3u*Ba8ue0*nj*-k#Fqpw7(uS&;T%9#%%g!9-yJ&^Q`Rh3#u8(I_dO|8cc1}H6 z`X@yMp)f;gymZaekpEuGWf1~j(6`qq+u}bU^-EN^kJ*RzBU_W=T`4NBSUxzHkK7Uk z4sRUFzW3-uR)E?9Ge!S37j=*`lcfip8X732Ip9#|@(oGWl!eH=T`wMhPvi9mWx{8>ddpcbus&t#SXsKI#<$w0Qy%pZU|*ky_JB z++dUQ0I;=_TrBvlfY$K6KFY%B>xFtrb6D^&UVH7qCRIJZg3IMeo?Z?vH_h?S%oePR z#D_-T&!viM$4}mt3s6`x>V1fwxxp@<*q5LzGo-su6G|V((SdZ%@%m4bCzDw2Ty0V0*l+|i`^ z!ZoUAnxw@ZQ2R@=lT1gGtt>ZED2U|5_u?3i%zh!JPgrW7t%GWmn)^T=1VX*YWE^7X zQNm#5(Noz(P-Z662W69c@mAU1ZHjN}1`FXd%n8_Fsi1xfT#M&mc`T|^v zZ_BQx3cRI$(kSn_#;_`je(PGk>}l4?k9MY6sU#`;szFpbOQ`R*{kZzLT7NC8J6b1o z81t1oZhtmZ`!m&;XZ&?0R5P=zs&b~|L5njCNAy{VGDt`x8!#uXM=G=I{aT$sqfbZ( z4nZkWKWx!2%D5ImB+Dul^oR@1?T=$n2M!D-7feQtB!z*u*@>uuAs}@ywh@f(Y2WxM z9&=IqN$+qoBEAI_oUYkxdGyi)VOTjpuXl^ii@<9Y9;+%~rNGa7PP zspDk7=pT#n*Jvy<=O_;px$L0ja)r<+U>h4`3!=GbY`wm>%{!e90so4H3F;8V4%Pl} zxSYht{7K@d*)=G`B8+&X;aAfDkB*ICt%A$q_qhjpb?wkEI z*v>yS^Os_=yDi6948n$RAsBo_rQZ(H3a&oX!-0R-h$8)zPJj>iAtKyNiml4-JQt3Z zZq}d7m<7ZbiqIy;i4+NT%J)8s_c$dctU0^fvjX}C;LG);%mzy+=}-PK3Vh8Lgs--5 zQN~1TiyJJLQs@tH@5Hh^RbptZfu4b1ZQ)-xaFs7!N_H5cgS>o21ei>_37z$1diJbP zjGEQd8aD5h4l2tKsm!jF&bo^R5pa0mScYeGJ+)!&gEAj*+9Gq$!e{+?oz%(F90&)? zw1x3jsrX57NJ7*4knZC-coDt(Lz7aogz3aYfxQZb2I#VZvKojA#5hM>O3UV=CyNxL z@_`wTc1c`TxxikldYes6;qRxu9&D-m9iT^PGqVe3GW!RZ{r#_5or;h8H_YM)5!<(u z&K!%|7DAU{e(pwy+C`IgQre72syg8W^KAcvO2SVc|G3}kryfR-l9t9#!KK)?9^<#| zzmya?48i5Y17pyMaBao2n_tTBKly z>^mRUi7J$+R#tyh^Lp@vHS!J!c0VUEzRptZ1rFY!B>GEt6d$xfxO~02Sq%sf15>iofd!rkGA56W|DD$)W@jRQ~B^-|z%?MUy@ zUo15)4H2!suziv1zleqHH=4%THiH`w@Uj{~X2pp)MOR-`|Hl#q5LIi)E8{HNu}O^R z5PSdmPDt%g$rp_`L$`tgiWhME20;ORBVOhm=sHY`QI3`sfAhl7RbLBJyx9em$u#94 ziOu)mF|t^*Yy91Alp;+bH_wvo zyO)^2__1|4ipt9SM?5uT* z(#iF_fx9t!iUHIovaZxIAZAHC+i8mPC)@gL3fqyQl`Vlx1`TZoV!)yV+$<@@AT7hT;&=J>cVW;k zdH>?(uVzyd6q6zW9>nu;&k9Ikw+UZqj1z&cUmq@LN`wCm;ax7!E<|5T2!ZcB3Tt^R zlT?uDKb5zCeP%ygs}kjNmH>Qa@Ny3dDX9}cYB$q)F7hGrmT#4vw#~lw;KlDpL(@## zdhpgEyDH4xJU_>EQ2{qLc3H?&B>>c(h=%F>ogq?#6x{(vu7Q)utBfWYI0a$5v)L@Q z9*mL>^-kz|Y?IhREL40}#l=ArP9x2z;T1)UOL*L(39t*L5JA&w&VtKansrPJHu)9CXRjfM#kYY`#Tx8cm{t26*5+9Qd9> z9kPt;!%T>OFwHYe;cQ-$l!*0~grR=xx%dwx77zo**&%h%U!cGoMPAXtvAB^FJOT>e z$RR(`l3ZwgJ8Vi=&?1Dlo^l>G*%91;9_J+X^X%y>D$?@G<@YP+9MpzwKN;I5~)>;ne!Jrgz zf(a-+Cr0c>0aetX@K-871Y>u^;39Nad1t-a^(?2%;RUbjr7tHA;X;{^wAm=JT>r#Q z%&yji1=JH;>?zoJd>6BOIK<5ysdTOV(I)>|Fjy7Kci02ywX%V96v) { + return await navigator.clipboard.readText(); + }); +} + +test('Renders Max Depth /0 to /32', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').press('Shift+Home'); + await page.getByLabel('Network Address').fill('0.0.0.0'); + await page.getByLabel('Network Address').press('Tab'); + await page.getByLabel('Network Size').fill('0'); + await page.getByRole('button', { name: 'Go' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/0 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/1 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/2 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/3 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/4 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/5 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/6 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/7 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/8 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/9 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/10 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/11 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/12 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/13 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/14 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/15 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/16 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/17 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/18 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/19 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/20 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/21 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/22 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/23 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/24 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/25 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/26 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/27 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/28 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/29 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/30 Split' }).click(); + await page.getByRole('cell', { name: '0.0.0.0/31 Split' }).click(); + await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Subnet Address')).toContainText('0.0.0.0/32'); + await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Range of Addresses')).toContainText('0.0.0.0'); + await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Usable IPs')).toContainText('0.0.0.0'); + await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Hosts')).toContainText('1'); + await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/32'); + await expect(page.getByLabel('/31 Join')).toContainText('/31'); + await expect(page.getByLabel('128.0.0.0/1', { exact: true }).getByLabel('Subnet Address')).toContainText('128.0.0.0/1'); +}); + +test('Change To 192.168.0.0/24', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').fill('192.168.0.0'); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('24'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24'); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Range of Addresses')).toContainText('192.168.0.0 - 192.168.0.255'); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Usable IPs')).toContainText('192.168.0.1 - 192.168.0.254'); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Hosts')).toContainText('254'); + await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/24'); + await expect(page.getByRole('textbox', { name: '192.168.0.0/24 Note' })).toBeEmpty(); +}); + +test('Deep /32 Split', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByText('/16', { exact: true }).click(); + await page.getByLabel('10.0.128.0/17', { exact: true }).getByText('/17', { exact: true }).click(); + await page.getByLabel('10.0.128.0/18', { exact: true }).getByText('/18', { exact: true }).click(); + await page.getByLabel('10.0.160.0/19', { exact: true }).getByText('/19', { exact: true }).click(); + await page.getByLabel('10.0.176.0/20', { exact: true }).getByText('/20', { exact: true }).click(); + await page.getByLabel('10.0.184.0/21', { exact: true }).getByText('/21', { exact: true }).click(); + await page.getByLabel('10.0.176.0/21', { exact: true }).getByText('/21', { exact: true }).click(); + await page.getByLabel('10.0.176.0/22', { exact: true }).getByText('/22', { exact: true }).click(); + await page.getByLabel('10.0.176.0/23', { exact: true }).getByText('/23', { exact: true }).click(); + await page.getByLabel('10.0.176.0/24', { exact: true }).getByText('/24', { exact: true }).click(); + await page.getByLabel('10.0.176.0/25', { exact: true }).getByText('/25', { exact: true }).click(); + await page.getByLabel('10.0.176.0/26', { exact: true }).getByText('/26', { exact: true }).click(); + await page.getByLabel('10.0.176.0/27', { exact: true }).getByText('/27', { exact: true }).click(); + await page.getByLabel('10.0.176.0/28', { exact: true }).getByText('/28', { exact: true }).click(); + await page.getByLabel('10.0.176.0/29', { exact: true }).getByText('/29', { exact: true }).click(); + await page.getByLabel('10.0.176.0/30', { exact: true }).getByText('/30', { exact: true }).click(); + await page.getByLabel('10.0.176.0/31', { exact: true }).getByText('/31', { exact: true }).click(); + await page.getByRole('textbox', { name: '10.0.176.0/32 Note' }).click(); + await page.getByRole('textbox', { name: '10.0.176.0/32 Note' }).fill('Test Text'); + await page.getByText('Change Colors »').click(); + await page.locator('#palette_picker_6').click(); + await page.getByRole('cell', { name: '10.0.176.0/32 Subnet Address' }).click(); + await page.getByText('« Stop Changing Colors').click(); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').fill('99.0.0.0'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Subnet Address')).toContainText('99.0.176.0/32'); + await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Hosts')).toContainText('1'); + await expect(page.getByRole('textbox', { name: '99.0.176.0/32 Note' })).toHaveValue('Test Text'); + await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/32'); +}); + +test('Usable IPs - Standard', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Standard' }).click(); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254'); +}); + +test('Usable IPs - AWS', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.4 - 10.0.255.254'); +}); + +test('Usable IPs - Azure', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Azure' }).click(); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.4 - 10.0.255.254'); +}); + +test('Note Splitting', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Note Split/Join').click(); + await page.getByLabel('Note Split/Join').fill('This should be duplicated!'); + await page.getByRole('cell', { name: '/16 Split' }).click(); + await expect(page.getByRole('textbox', { name: '10.0.0.0/17 Note' })).toHaveValue('This should be duplicated!'); + await expect(page.getByRole('textbox', { name: '10.0.128.0/17 Note' })).toHaveValue('This should be duplicated!'); +}); + +test('Note Joining Same', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0B2qAKvQJakAEp9A9gFcANgBNuSAKbdRggA7COAYwCGAF0miAhCAC+iNI0igW0dl14CR4qTPmLVG7Xt2ugA'); + await page.getByLabel('/16 Join').click(); + await expect(page.getByLabel('Note Split/Join')).toHaveValue('This should be duplicated!'); +}); + +test('Note Joining Different', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0B2qAKvQJakAEp9A9gFcANgBNuSAKbdRggA7COAYwCGAF0miAhNwCCIAL6I0jSKBbR2XXgJHipM+YtUbt3AEKGD3oA'); + await page.getByLabel('/16 Join').click(); + await expect(page.getByLabel('Note Split/Join')).toBeEmpty(); +}); + +test('Color Splitting', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByText('Change Colors »').click(); + await page.getByLabel('Color 5').click(); + await page.getByRole('cell', { name: '/16 Subnet Address' }).click(); + await page.getByText('« Stop Changing Colors').click(); + await page.getByText('/16', { exact: true }).click(); + await expect(page.getByRole('row', { name: '10.0.0.0/17' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.128.0/17' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); +}); + +test('Color Joining Same', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0DGqAxAJxIBmhXXIAX0RpGkUC2gduvfgLkCgA'); + await page.getByLabel('/16 Join').click(); + await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); +}); + +test('Color Joining Different', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0DGqAxAJxIBmhXXIAX0RpGkUC2isuAEz5JiAxQKA'); + await page.getByLabel('/16 Join').click(); + await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); +}); + +//test('Test', async ({ page }) => { +// await page.goto('https://127.0.0.1:8443/'); +//}); diff --git a/src/tests/ui-error-handling.spec.ts b/src/tests/ui-error-handling.spec.ts new file mode 100644 index 0000000..fc17d17 --- /dev/null +++ b/src/tests/ui-error-handling.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '@playwright/test'; + +test('Bad Network Address', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').fill('1'); + await page.locator('html').click(); + await expect(page.locator('#network')).toHaveClass(/error/i); + await expect(page.getByText('Must be a valid IPv4 Address')).toBeVisible(); +}); + +test('Bad Network Size', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('33'); + await page.locator('html').click(); + await expect(page.locator('#netsize')).toHaveClass(/error/i); + await expect(page.getByText('Smallest size is /32')).toBeVisible(); +}); + +test('Prevent Go on Bad Input', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('33'); + await page.locator('html').click(); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.locator('#notifyModalLabel')).toContainText('Warning!'); + await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!'); +}); + + +test('Network Boundary Correction', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').fill('123.45.67.89'); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('20'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.locator('#notifyModalLabel')).toContainText('Warning!'); + await expect(page.locator('#notifyModalDescription')).toContainText('Your network input is not on a network boundary for this network size. It has been automatically changed:'); + await expect(page.locator('#notifyModalDescription')).toContainText('123.45.67.89 -> 123.45.64.0'); + await page.getByLabel('Warning!').getByLabel('Close').click(); + await expect(page.getByLabel('Network Address')).toHaveValue('123.45.64.0'); + await page.getByLabel('Network Size').click(); + await expect(page.getByRole('cell', { name: '123.45.64.0/20 Subnet Address' })).toContainText('123.45.64.0/20'); + await page.getByRole('cell', { name: '/20 Split' }).click(); + await page.getByLabel('/20 Join').click(); + await expect(page.getByLabel('123.45.64.0/20', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/20'); +}); + +test('Subnet Too Small for AWS Mode', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('29'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.locator('#notifyModalLabel')).toContainText('Warning!'); + await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!'); + await expect(page.getByText('AWS Mode - Smallest size is /28')).toBeVisible(); +}); + +test('Subnet Too Small for Azure Mode', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Azure' }).click(); + await page.getByLabel('Network Size').click(); + await page.getByLabel('Network Size').fill('30'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.locator('#notifyModalLabel')).toContainText('Warning!'); + await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!'); + await expect(page.getByText('Azure Mode - Smallest size is /29')).toBeVisible(); +}); + diff --git a/src/tests/ui-usage.spec.ts b/src/tests/ui-usage.spec.ts new file mode 100644 index 0000000..ee7031a --- /dev/null +++ b/src/tests/ui-usage.spec.ts @@ -0,0 +1,168 @@ +import { test, expect } from '@playwright/test'; + +test('CIDR Input Typing', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').press('End'); + await page.getByLabel('Network Address').press('Shift+Home'); + await page.getByLabel('Network Address').press('Delete'); + await page.keyboard.type('192.168.0.0/24'); + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.getByRole('cell', { name: '192.168.0.0/24 Subnet Address' })).toContainText('192.168.0.0/24'); +}); + +test('CIDR Input Paste', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByLabel('Network Address').click(); + await page.getByLabel('Network Address').press('End'); + await page.getByLabel('Network Address').press('Shift+Home'); + // From: https://github.com/microsoft/playwright/issues/2511 + await page.locator('#network').evaluate((formEl) => { + const data = `172.16.0.0/12`; + const clipboardData = new DataTransfer(); + const dataType = 'text/plain'; + clipboardData.setData(dataType, data); + const clipboardEvent = new ClipboardEvent('paste', { + clipboardData, + dataType, + data + }); + formEl.dispatchEvent(clipboardEvent); + }); + + await page.getByRole('button', { name: 'Go' }).click(); + await expect(page.getByRole('cell', { name: '172.16.0.0/12 Subnet Address' })).toContainText('172.16.0.0/12'); +}); + +test('About Dialog', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.locator('#info_icon').click(); + await expect(page.locator('#aboutModalLabel')).toContainText('About Visual Subnet Calculator'); + await expect(page.getByLabel('About Visual Subnet Calculator')).toContainText('Design Tenets'); + await expect(page.getByLabel('About Visual Subnet Calculator')).toContainText('Credits'); + await expect(page.getByLabel('About Visual Subnet Calculator').getByText('Close')).toBeVisible(); +}); + +test('GitHub Link', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + const page1Promise = page.waitForEvent('popup'); + await page.getByLabel('GitHub').click(); + const page1 = await page1Promise; + await expect(page1.locator('#repository-container-header')).toContainText('ckabalan / visualsubnetcalc Public'); +}); + +test('Table Header Standard Mode', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); +}); + +test('Table Header AWS Mode', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (AWS)'); + await page.getByRole('link', { name: 'AWS' }).hover() + await expect(page.getByText('AWS reserves 5 addresses in')).toBeVisible(); +}); + +test('Table Header Azure Mode', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Azure' }).click(); + await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (Azure)'); + await page.getByRole('link', { name: 'Azure' }).hover() + await expect(page.getByText('Azure reserves 5 addresses in')).toBeVisible(); +}); + + +test('Table Header AWS then Standard', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - AWS' }).click(); + await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (AWS)'); + await page.getByRole('button', { name: 'Tools' }).click(); + await page.getByRole('link', { name: 'Mode - Standard' }).click(); + await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs'); + await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).not.toContainText('(AWS)'); +}); + +test('Color Palette', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »'); + await page.getByText('Change Colors »').click(); + await expect(page.getByLabel('Color 1', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 2', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 3', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 4', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 5', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 6', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 7', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 8', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 9', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Color 10', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Stop Changing Colors').locator('span')).toContainText('« Stop Changing Colors'); + await page.getByText('« Stop Changing Colors').click(); + await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »'); +}); + +test('Test Default Colors', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0YCWTAVkwNYUC+ia3SMB590HIbEHDEAZikjRaVhJjjQAFnmIArPNEy1IQlpAB2PSP6MVyjYYAcJgJx6dhzCbQDelkDNtG7jCecj6Ipu6avPy6PgoiQA'); + await page.getByText('Change Colors »').click(); + // Set the top 10 rows to the default colors and check that they are correct + await page.getByLabel('Color 1', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.0.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.0.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)'); + await page.getByLabel('Color 2', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.16.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.16.0/20' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await page.getByLabel('Color 3', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.32.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.32.0/20' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await page.getByLabel('Color 4', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.48.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.48.0/20' })).toHaveCSS('background-color', 'rgb(202, 255, 191)'); + await page.getByLabel('Color 5', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.64.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.64.0/20' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await page.getByLabel('Color 6', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.80.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.80.0/20' })).toHaveCSS('background-color', 'rgb(160, 196, 255)'); + await page.getByLabel('Color 7', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.96.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.96.0/20' })).toHaveCSS('background-color', 'rgb(189, 178, 255)'); + await page.getByLabel('Color 8', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.112.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.112.0/20' })).toHaveCSS('background-color', 'rgb(255, 198, 255)'); + await page.getByLabel('Color 9', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.128.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.128.0/20' })).toHaveCSS('background-color', 'rgb(230, 230, 230)'); + await page.getByLabel('Color 10', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.144.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.144.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + // Set rows 11 and 12 to Colors 1 and 2 respectively and check that they are correct + await page.getByLabel('Color 1', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.160.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.160.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)'); + await page.getByLabel('Color 2', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.176.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.176.0/20' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + // Set rows 11 and 12 to Color 10 (white) to make sure you can change colors later + await page.getByLabel('Color 10', { exact: true }).click(); + await page.getByRole('cell', { name: '10.0.160.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.160.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + await page.getByRole('cell', { name: '10.0.176.0/20 Subnet Address' }).click(); + await expect(page.getByRole('row', { name: '10.0.176.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + await page.getByText('« Stop Changing Colors').click(); + // Make sure when you're not in color change mode you cannot change colors + await page.getByRole('cell', { name: '10.0.0.0/20 Subnet Address' }).click(); + // Should still be the old color instead of white (the last palette color selected) + await expect(page.getByRole('row', { name: '10.0.0.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)'); +}); + + + + + diff --git a/src/tests/url-sharing.spec.ts b/src/tests/url-sharing.spec.ts new file mode 100644 index 0000000..44fd975 --- /dev/null +++ b/src/tests/url-sharing.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '@playwright/test'; + +async function getClipboardText(page) { + return page.evaluate(async () => { + return await navigator.clipboard.readText(); + }); +} + +test('Default URL Share', async ({ page }) => { + await page.goto('https://127.0.0.1:8443/'); + await page.getByText('Copy Shareable URL').click(); + const clipboardUrl = await getClipboardText(page); + expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hQL71A'); +}); + +test('Default URL Render', async ({ page }) => { + // This should match default-homepage.spec.ts + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hQL71A'); + await expect(page).toHaveTitle(/Visual Subnet Calculator/); + await expect(page.getByRole('heading')).toContainText('Visual Subnet Calculator'); + await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('16'); + await expect(page.locator('#useableHeader')).toContainText('Usable IPs'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/16'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Range of Addresses')).toContainText('10.0.0.0 - 10.0.255.255'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254'); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Hosts')).toContainText('65534'); + await expect(page.getByRole('textbox', { name: '10.0.0.0/16 Note' })).toBeEmpty(); + await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16'); + // This "default no color" check could maybe be improved. May not be reliable cross-browser. + await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); + await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »'); + await expect(page.locator('#copy_url')).toContainText('Copy Shareable URL'); +}); + +test('Reddit Example URL Render (URL v1 - Config v1)', async ({ page }) => { + // This is great to make sure older URLs still load and render properly + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIIwgNCAzlUMAMA6LOD0AmdVWHbbAuSNUvffYjM2gZgZvPwBZiB9AOyggAIgEMALiIAEAYQCmfMbIBOkgLSSAagEslYgK4iANpIDKysMpSIeAY0EBiAJwAjAGYA2V65ABfRIywYDm4qEH5BUQkZeUUVdW1dA2MzJQslKzC7aCc3T28fPxIyfA5WUIDMEvQCENBw6EipOQVlNU0dfSNTc0sETIcXDy9ff1JmYN4BBvEmmNb1AAUACwBPJC0bLpS0jNsHEXQbTmGCworODnpy0gvq-DK6qZAAIT0tQwATLT4Ac0kAQTaAHUtK4tH09tkvB93CIAKwjIpYdylSaCV7vL6-AFtAAy-wAchCsiB7NDYQjTqMyAAODiUai0y5sJl3B5IzB0u61MJPDGfb5-QGLJTfWK7Elk1ww+GIiqOCaheovN4C7HCzQAew2smJDnJsoK1MCLDR0H5WL+z2BoPB1kl0q8zncvjOpBgVQIV0ZgU99zNKsxgsk1vU+KJ9v1HydLrdZBgtwI7IqCcVj3RqstIbaC1FLXSeqh0dczrl7rhad5GaD2NDWp1hdJjpLsdOpyAA'); + await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('20'); + await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24'); + await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24'); + await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24'); + await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24'); + await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23'); + await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23'); + await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24'); + await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24'); + await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23'); + await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23'); + await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24'); + await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24'); + await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers'); + await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice'); + await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice'); + await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)'); + await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); +}); + +test('Reddit Example URL Conversion (URL v1 - Config v1 to v2)', async ({ page }) => { + // Basically if a user loads a URL (say v1), we load it, but then only produce the latest version URL (v2) when they copy the URL + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIIwgNCAzlUMAMA6LOD0AmdVWHbbAuSNUvffYjM2gZgZvPwBZiB9AOyggAIgEMALiIAEAYQCmfMbIBOkgLSSAagEslYgK4iANpIDKysMpSIeAY0EBiAJwAjAGYA2V65ABfRIywYDm4qEH5BUQkZeUUVdW1dA2MzJQslKzC7aCc3T28fPxIyfA5WUIDMEvQCENBw6EipOQVlNU0dfSNTc0sETIcXDy9ff1JmYN4BBvEmmNb1AAUACwBPJC0bLpS0jNsHEXQbTmGCworODnpy0gvq-DK6qZAAIT0tQwATLT4Ac0kAQTaAHUtK4tH09tkvB93CIAKwjIpYdylSaCV7vL6-AFtAAy-wAchCsiB7NDYQjTqMyAAODiUai0y5sJl3B5IzB0u61MJPDGfb5-QGLJTfWK7Elk1ww+GIiqOCaheovN4C7HCzQAew2smJDnJsoK1MCLDR0H5WL+z2BoPB1kl0q8zncvjOpBgVQIV0ZgU99zNKsxgsk1vU+KJ9v1HydLrdZBgtwI7IqCcVj3RqstIbaC1FLXSeqh0dczrl7rhad5GaD2NDWp1hdJjpLsdOpyAA'); + await page.getByText('Copy Shareable URL').click(); + const clipboardUrl = await getClipboardText(page); + expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9DBgiAM5SgYDW5IGANjRgLaMB2jA9je9ACICGAF34ACAMIBTVoIkAnEQFoRANQCWswQFd+dEQGU5YOWUQBjVAGIAnEgBmANlu2QAX0RoukUDxADh4qRl5JTUNbV0DWSNZExBzaGs7R2cXN3QeUBhPb1Q-UUlpOUUVdS0dfUNjYniQRIcnV0QAZmyQHzyAwuCRAAUACwBPElVTcsjo2JqLfgxTABYG1LS0Fi9YDLbUACFNVToAE1VWAHMRAEFigHVVW1Vqyyd9+34AVkaQJo2fHb3Dk-PigAZM4AOXuCUezzeS3cDDWMFWoDmGwAHK1vrsDkdThclD1ZEcgpMHrYnq93lZ0dtMX8ccVlBwRhJwbVIeTUogXl9qb9sSItlcbnczA99k4kPZXGkmoiQPZudAflj-gKlMCwSKIWLbBL3gB2DZoOZUxU0vmq3oErrErXiyXLF4mkBK2n8+mM0zMzWs7W6pb+oA'); +}); + +test('Reddit Example URL Render (URL v1 - Config v2)', async ({ page }) => { + // This is great to make sure older URLs still load and render properly + await page.goto('https://127.0.0.1:8443/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9DBgiAM5SgYDW5IGANjRgLaMB2jA9je9ACICGAF34ACAMIBTVoIkAnEQFoRANQCWswQFd+dEQGU5YOWUQBjVAGIAnEgBmANlu2QAX0RoukUDxADh4qRl5JTUNbV0DWSNZExBzaGs7R2cXN3QeUBhPb1Q-UUlpOUUVdS0dfUNjYniQRIcnV0QAZmyQHzyAwuCRAAUACwBPElVTcsjo2JqLfgxTABYG1LS0Fi9YDLbUACFNVToAE1VWAHMRAEFigHVVW1Vqyyd9+34AVkaQJo2fHb3Dk-PigAZM4AOXuCUezzeS3cDDWMFWoDmGwAHK1vrsDkdThclD1ZEcgpMHrYnq93lZ0dtMX8ccVlBwRhJwbVIeTUogXl9qb9sSItlcbnczA99k4kPZXGkmoiQPZudAflj-gKlMCwSKIWLbBL3gB2DZoOZUxU0vmq3oErrErXiyXLF4mkBK2n8+mM0zMzWs7W6pb+oA'); + await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0'); + await expect(page.getByLabel('Network Size')).toHaveValue('20'); + await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24'); + await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24'); + await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24'); + await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24'); + await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23'); + await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23'); + await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24'); + await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24'); + await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23'); + await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23'); + await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24'); + await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24'); + await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers'); + await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers'); + await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice'); + await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi'); + await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN'); + await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers'); + await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice'); + await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)'); + await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)'); + await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)'); + await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); + await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)'); +}); + + +//test('Test', async ({ page }) => { +// await page.goto('https://127.0.0.1:8443/'); +//}); +