Merge branch 'release/v1.3.0'

This commit is contained in:
Caesar Kabalan 2024-10-15 15:48:05 -07:00
commit 8bacdbfafd
No known key found for this signature in database
GPG key ID: DDFEF5FF6CFAB608
27 changed files with 2142 additions and 573 deletions

View file

@ -1,38 +1,33 @@
name: docker name: Docker Build
on: on:
push: #push:
branches: # branches:
- 'develop' # - 'develop'
- 'main' # - 'main'
workflow_dispatch:
env: env:
DOCKERHUB_TAG: ${{ github.ref_name == 'main' && 'latest' || 'develop' }} DOCKERHUB_TAG: ${{ github.ref_name == 'main' && 'latest' || 'develop' }}
jobs: jobs:
docker: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Set up QEMU
name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- - id: container_name
id: container_name
uses: ASzc/change-string-case-action@v5 uses: ASzc/change-string-case-action@v5
with: with:
string: ${{ github.repository_owner }}/${{ github.event.repository.name }} string: ${{ github.repository_owner }}/${{ github.event.repository.name }}
- - name: Build and push
name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
push: true push: true

43
.github/workflows/ui-testing.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: UI Testing (Playwright)
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- run: |
export MKCERT_VERSION=v1.4.4
export PLATFORM=linux
curl -L "https://github.com/FiloSottile/mkcert/releases/download/$MKCERT_VERSION/mkcert-$MKCERT_VERSION-$PLATFORM-amd64" -o mkcert
chmod +x mkcert
echo "$PWD/mkcert" >> $GITHUB_PATH
mkdir -p src/certs
./mkcert -cert-file src/certs/cert.pem -key-file src/certs/cert.key localhost 127.0.0.1
- run: npm ci
working-directory: ./src
- run: npm run build --if-present
working-directory: ./src
- name: Install Playwright Browsers
run: npx playwright install --with-deps
working-directory: ./src
- name: Run Playwright tests
run: npm test
working-directory: ./src
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: ./src/playwright-report
retention-days: 30

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.idea/* .idea/*
**/node_modules/* **/node_modules/*
**/certs/* **/certs/*
**/.DS_Store

107
.repopackignore Normal file
View file

@ -0,0 +1,107 @@
# VisualSubnetCalc Custom
dist/css/bootstrap.*
src/cloudformation.yaml
# RepoPack Defaults
# Version control
.git/**
.hg/**
.hgignore
.svn/**
# Dependency directories
node_modules/**
**/node_modules/**
bower_components/**
**/bower_components/**
jspm_packages/**
**/jspm_packages/**
vendor/**
.bundle/**
.gradle/**
target/**
# Logs
logs/**
**/*.log
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
# Runtime data
pids/**
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov/**
# Coverage directory used by tools like istanbul
coverage/**
# nyc test coverage
.nyc_output/**
# Grunt intermediate storage
.grunt/**
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release/**
# TypeScript v1 declaration files
typings/**
# Optional npm cache directory
**/.npm/**
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn files
**/.yarn/**
# Yarn Integrity file
**/.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next/**
# nuxt.js build output
.nuxt/**
# vuepress build output
.vuepress/dist/**
# Serverless directories
.serverless/**
# FuseBox cache
.fusebox/**
# DynamoDB Local files
.dynamodb/**
# TypeScript output
#dist/**
# OS generated files
**/.DS_Store
**/Thumbs.db
# Editor directories and files
.idea/**
.vscode/**
**/*.swp
**/*.swo
**/*.swn
**/*.bak
# Package manager locks
**/package-lock.json
**/yarn.lock
**/pnpm-lock.yaml
# Build outputs
build/**
out/**
# Temporary files
tmp/**
temp/**
# repopack output
repopack-output.txt
# Essential Python-related entries
**/__pycache__/**
**/*.py[cod]
**/venv/**
**/.venv/**
**/.pytest_cache/**
**/.mypy_cache/**
**/.ipynb_checkpoints/**
**/Pipfile.lock
**/poetry.lock

View file

@ -11,3 +11,4 @@ RUN npm run build
FROM nginx FROM nginx
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html

View file

@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

57
NOTES.md Normal file
View file

@ -0,0 +1,57 @@
# Notes
## Efficient Saves
Looks like the most efficient way to store an address is to track the base network (say 10.0.0.0/8) and then represent
all other addresses as offsets from that base network. This way, we can store a subnet as a base network and a mask. The
most efficient way to store this is to store two values:
- The base network offset (0 to 4,294,967,296)
- 0 being the best case, a subnet within the base subnet with the same network address (10.0.0.0/24)
- 255.255.255.255 being the worst case, the last address in 0.0.0.0/0 (unrealistic)
- The mask (0 to 32)
Combine both of these values into a single binary string that is 32+5 bits. Round the storage up to the nearest byte
(40 bits = 5 bytes), padding the remaining bits with 0s, then encode this 5 byte string into a URL-safe base64 string.
Example for 10.0.0.0/8 and representing the network 10.0.15.0/24:
Find the offset:
10.0.0.0 (decimal):
00001010 00000000 00000000 00000000 → 167772160
10.0.15.0 (decimal):
00001010 00000000 00001111 00000000 → 167775232
Offset: 167775232 - 167772160 = 3072
Hmmm, this above works good for close together smaller networks but gets ugly when you're dealing with larger networks
because the offset is huge.
I'm thinking about a coordinate system. Let's say you have a base network of 10.0.0.0/8 and you want to as concisely as
possible represent 10.166.64.0/20. We could represent this as the nth /20 within the /8 and just store N-20, and
probably shorten that even more with the last digit always being base32 for the mask.
If I'm doing my math right you can say a /20 has 4096 addresses.
10.0.0.0 = 167772160
10.166.64.0 = 178667520
178667520 - 167772160 = 10895360
10895360 / 4096 = 2660
Which means 10.166.64.0 is the 2,660th /20 within the /8. You could store this as 2660x20 (inefficient)
You can reverse this 2660x20 from 10.0.0.0/8 by doing the following:
a /21 has 4096 addresses, times the 2660th network is 10895360. Add that to the base network address to get the network
10.0.0.0 = 167772160
167772160 + 10895360 = 178667520
178667520 = 10.166.64.0 then you can tack back on the /20
This has the advantage overall that you're unlikely to store wildly different subnet sizes in the same page. Your "N" in
the "Nth" subnet is likely to be very small. You're more likely to store the Nth /24 in a /20 than you are a /20 in an
/8. So the numbers will mostly be 1-2 digits, rarely 3 digits, and almost never 4 digits as depicted above.
I then for efficienty I could use this format:
`[Nth Network as Integer][Network Size as Base32]`
So lets say you're wanting to represent the 0th /24 in a /20 you would represent it as `00`, always knowing the last
digit is the network size. Or the 0th /32 would be `07` (32 in base32 is 7). or the 5th /28 would be `54`.

View file

@ -33,10 +33,12 @@ Compile from source:
```shell ```shell
# Clone the repository # Clone the repository
> git clone https://github.com/ckabalan/visualsubnetcalc > git clone https://github.com/ckabalan/visualsubnetcalc
# Change to the repository directory
> cd visualsubnetcalc
# Use recommended NVM version # Use recommended NVM version
> nvm use > nvm use
# Change to the sources directory # Change to the sources directory
> cd visualsubnetcalc/src > cd src
# Install Bootstrap # Install Bootstrap
> npm install > npm install
# Compile Bootstrap (Also install sass command line globally) # Compile Bootstrap (Also install sass command line globally)
@ -105,3 +107,4 @@ Split icon made by [Freepik](https://www.flaticon.com/authors/freepik) from [Fla
## License ## License
Visual Subnet Calculator is released under the [MIT License](https://opensource.org/licenses/MIT) Visual Subnet Calculator is released under the [MIT License](https://opensource.org/licenses/MIT)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15
dist/css/main.css vendored
View file

@ -51,6 +51,21 @@
border-bottom:1px black dotted; border-bottom:1px black dotted;
} }
#whats_new {
cursor:pointer !important;
text-align: right;
width:15rem;
float:right;
}
#whats_new a {
width:15rem;
text-align: right;
text-decoration: none;
border-bottom:1px var(--bs-success) dotted;
}
#copy_url { #copy_url {
cursor:pointer !important; cursor:pointer !important;
text-align: center; text-align: center;

78
dist/index.html vendored
View file

@ -19,12 +19,12 @@
<body> <body>
<div class="container-xxl mt-3"> <div class="container-xxl mt-3">
<div class="float-end" id="navigation"> <div class="float-end" id="navigation">
<a href="#" id="info_icon" data-bs-toggle="modal" data-bs-target="#aboutModal"> <a href="#" id="info_icon" data-bs-toggle="modal" data-bs-target="#aboutModal" aria-label="About">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/> <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg> </svg>
</a><a href="https://github.com/ckabalan/visualsubnetcalc" target="_blank"> </a><a href="https://github.com/ckabalan/visualsubnetcalc" target="_blank" aria-label="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg> </svg>
@ -95,78 +95,80 @@
<table id="calc" class="table table-bordered font-monospace"> <table id="calc" class="table table-bordered font-monospace" aria-label="Subnet Table">
<thead> <thead>
<tr> <tr>
<th id="subnetHeader" style="display: table-cell;">Subnet address</th> <th aria-label="Subnet Address" id="subnetHeader" style="display: table-cell;">Subnet Address</th>
<th id="netmaskHeader" style="display: none;">Netmask</th> <th aria-label="Range of Addresses" id="rangeHeader" style="display: table-cell;">Range of Addresses</th>
<th id="rangeHeader" style="display: table-cell;">Range of addresses</th> <th aria-label="Usable IPs" id="useableHeader" style="display: table-cell;">Usable IPs</th>
<th id="useableHeader" style="display: table-cell;">Usable IPs</th> <th aria-label="Hosts" id="hostsHeader" style="display: table-cell;">Hosts</th>
<th id="hostsHeader" style="display: table-cell;">Hosts</th> <th aria-label="Note" id="noteHeader" colspan="100%" style="display: table-cell;">
<th id="noteHeader" colspan="100%" style="display: table-cell;">
Note Note
<div style="float:right;"><span class="split">Split</span>/<span class="join">Join</span></div> <div style="float:right;"><span id="splitHeader" aria-label="Split" class="split">Split</span>/<span id="joinHeader" aria-label="Join" class="join">Join</span></div>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody id="calcbody"> <tbody id="calcbody">
<tr id="row_10-0-0-0_17"> <tr id="row_10-0-0-0_16" aria-label="10.0.0.0/16">
<td class="row_address">Loading...</td> <td aria-labelledby="subnetHeader" class="row_address">Loading...</td>
<td class="row_range"></td> <td aria-labelledby="rangeHeader" class="row_range"></td>
<td class="row_usable"></td> <td aria-labelledby="useableHeader" class="row_usable"></td>
<td class="row_hosts"></td> <td aria-labelledby="hostsHeader" class="row_hosts"></td>
<td class="note"><label><input type="text" class="form-control shadow-none p-0"></label></td> <td class="note"><label><input aria-labelledby="noteHeader" type="text" class="form-control shadow-none p-0"></label></td>
<td rowspan="1" colspan="13" class="split rotate"><span></span></td> <td aria-labelledby="splitHeader" rowspan="1" colspan="13" class="split rotate"><span></span></td>
<td rowspan="14" colspan="1" class="join rotate"><span></span></td> <td aria-labelledby="joinHeader" rowspan="14" colspan="1" class="join rotate"><span></span></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div id="bottom_nav"> <div id="bottom_nav">
<div class="d-inline-block align-top pt-1" id="colors_word_open"><span>Change Colors &#187;</span></div> <div class="d-inline-block align-top pt-1" id="colors_word_open" aria-label="Change Colors"><span>Change Colors &#187;</span></div>
<div class="d-inline-block d-none" id="color_palette"> <div class="d-inline-block d-none" id="color_palette" aria-label="Color Palette">
<div id="palette_picker_1"></div> <div role="button" aria-label="Color 1" id="palette_picker_1"></div>
<div id="palette_picker_2"></div> <div role="button" aria-label="Color 2" id="palette_picker_2"></div>
<div id="palette_picker_3"></div> <div role="button" aria-label="Color 3" id="palette_picker_3"></div>
<div id="palette_picker_4"></div> <div role="button" aria-label="Color 4" id="palette_picker_4"></div>
<div id="palette_picker_5"></div> <div role="button" aria-label="Color 5" id="palette_picker_5"></div>
<div id="palette_picker_6"></div> <div role="button" aria-label="Color 6" id="palette_picker_6"></div>
<div id="palette_picker_7"></div> <div role="button" aria-label="Color 7" id="palette_picker_7"></div>
<div id="palette_picker_8"></div> <div role="button" aria-label="Color 8" id="palette_picker_8"></div>
<div id="palette_picker_9"></div> <div role="button" aria-label="Color 9" id="palette_picker_9"></div>
<div id="palette_picker_10"></div> <div role="button" aria-label="Color 10" id="palette_picker_10"></div>
</div> </div>
<div class="d-inline-block align-top align-top pt-1 ps-2 d-none" id="colors_word_close"><span>&#171; Stop Changing Colors</span></div> <div class="d-inline-block align-top align-top pt-1 ps-2 d-none" id="colors_word_close" aria-label="Stop Changing Colors"><span>&#171; Stop Changing Colors</span></div>
<div class="d-inline-block align-top pt-1 ps-3" id="copy_url"><span>Copy Shareable URL</span></div> <div class="d-inline-block align-top pt-1 ps-3" id="copy_url"><span>Copy Shareable URL</span></div>
<div class="d-inline-block align-top pt-1 ps-3" id="whats_new">
<a title="Released 2024-10-15" class="link-success" href="https://github.com/ckabalan/visualsubnetcalc/releases/tag/v1.3.0" target="_blank" aria-label="What's New">v1.3.0 (What's New?)</a>
</div>
</div> </div>
<div class="modal fade" id="notifyModal" tabindex="-1" aria-labelledby="notifyModalLabel" aria-hidden="true"> <div class="modal fade" id="notifyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-md"> <div class="modal-dialog modal-md">
<div class="modal-content alert-warning"> <div class="modal-content alert-warning" role="alertdialog" aria-labelledby="notifyModalLabel" aria-describedby="notifyModalDescription">
<div class="modal-header border-bottom-0 pb-1"> <div class="modal-header border-bottom-0 pb-1">
<h3 class="modal-title" id="notifyModalLabel">Warning!</h3> <h3 class="modal-title" id="notifyModalLabel">Warning!</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body pt-1"> <div class="modal-body pt-1" id="notifyModalDescription">
Notification Text Here Notification Text Here
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="importExportModal" tabindex="-1" aria-labelledby="importExportModalLabel" aria-hidden="true"> <div class="modal fade" id="importExportModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content"> <div class="modal-content" role="alertdialog" aria-labelledby="importExportModalLabel" aria-describedby="importExportModalDescription">
<div class="modal-header border-bottom-0 pb-1"> <div class="modal-header border-bottom-0 pb-1">
<h3 class="modal-title" id="importExportModalLabel">Import/Export</h3> <h3 class="modal-title" id="importExportModalLabel">Import/Export</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body pt-1"> <div class="modal-body pt-1">
<div class="alert alert-primary show mt-3" role="alert"> <div class="alert alert-primary show mt-3" id="importExportModalDescription">
Copy the content from the box below to EXPORT the current subnet configuration. Or, overwrite/paste a previously exported configuration into the box below and click IMPORT. Copy the content from the box below to EXPORT the current subnet configuration. Or, overwrite/paste a previously exported configuration into the box below and click IMPORT.
</div> </div>
<div class="form-floating font-monospace"> <div class="form-floating font-monospace">
<textarea class="form-control pt-3" id="importExportArea" style="height: 510px"></textarea> <textarea class="form-control pt-3" id="importExportArea" style="height: 510px" aria-label="Import/Export Content"></textarea>
<label for="importExportArea"></label> <label for="importExportArea"></label>
</div> </div>
</div> </div>

280
dist/js/main.js vendored
View file

@ -28,7 +28,7 @@ let operatingMode = 'Standard'
let previousOperatingMode = 'Standard' let previousOperatingMode = 'Standard'
let inflightColor = 'NONE' let inflightColor = 'NONE'
let urlVersion = '1' let urlVersion = '1'
let configVersion = '1' let configVersion = '2'
const netsizePatterns = { const netsizePatterns = {
Standard: '^([12]?[0-9]|3[0-2])$', Standard: '^([12]?[0-9]|3[0-2])$',
@ -42,12 +42,27 @@ const minSubnetSizes = {
AWS: 28, AWS: 28,
}; };
$('input#network').on('paste', function (e) {
let pastedData = window.event.clipboardData.getData('text')
if (pastedData.includes('/')) {
let [network, netSize] = pastedData.split('/')
$('#network').val(network)
$('#netsize').val(netSize)
}
e.preventDefault()
});
$("input#network").on('keydown', function (e) {
if (e.key === '/') {
e.preventDefault()
$('input#netsize').focus().select()
}
});
$('input#network,input#netsize').on('input', function() { $('input#network,input#netsize').on('input', function() {
$('#input_form')[0].classList.add('was-validated'); $('#input_form')[0].classList.add('was-validated');
}) })
$('#color_palette div').on('click', function() { $('#color_palette div').on('click', function() {
// We don't really NEED to convert this to hex, but it's really low overhead to do the // We don't really NEED to convert this to hex, but it's really low overhead to do the
// conversion here and saves us space in the export/save // conversion here and saves us space in the export/save
@ -138,7 +153,7 @@ $('#bottom_nav #copy_url').on('click', function() {
}) })
$('#btn_import_export').on('click', function() { $('#btn_import_export').on('click', function() {
$('#importExportArea').val(JSON.stringify(exportConfig(), null, 2)) $('#importExportArea').val(JSON.stringify(exportConfig(false), null, 2))
}) })
function reset() { function reset() {
@ -150,14 +165,47 @@ function reset() {
let rootCidr = rootNetwork + '/' + $('#netsize').val() let rootCidr = rootNetwork + '/' + $('#netsize').val()
if (cidrInput !== rootCidr) { if (cidrInput !== rootCidr) {
show_warning_modal('<div>Your network input is not on a network boundary for this network size. It has been automatically changed:</div><div class="font-monospace pt-2">' + $('#network').val() + ' -> ' + rootNetwork + '</div>') show_warning_modal('<div>Your network input is not on a network boundary for this network size. It has been automatically changed:</div><div class="font-monospace pt-2">' + $('#network').val() + ' -> ' + rootNetwork + '</div>')
$('#network').val(rootNetwork)
cidrInput = $('#network').val() + '/' + $('#netsize').val()
}
if (Object.keys(subnetMap).length > 0) {
// This page already has data imported, so lets see if we can just change the range
if (isMatchingSize(Object.keys(subnetMap)[0], cidrInput)) {
subnetMap = changeBaseNetwork(cidrInput)
} else {
// This is a page with existing data of a different subnet size, so make it blank
// Could be an opportunity here to do the following:
// - Prompt the user to confirm they want to clear the existing data
// - Resize the existing data anyway by making the existing network a subnetwork of their new input (if it
// is a larger network), or by just trimming the network to the new size (if it is a smaller network),
// or even resizing all of the containing networks by change in size of the base network. For example a
// base network going from /16 -> /18 would be all containing networks would be resized smaller (/+2),
// or bigger (/-2) if going from /18 -> /16.
subnetMap = {}
subnetMap[rootCidr] = {}
}
} else {
// This is a fresh page load with no existing data
subnetMap[rootCidr] = {}
} }
$('#network').val(rootNetwork)
subnetMap = {}
subnetMap[rootCidr] = {}
maxNetSize = parseInt($('#netsize').val()) maxNetSize = parseInt($('#netsize').val())
renderTable(operatingMode); renderTable(operatingMode);
} }
function changeBaseNetwork(newBaseNetwork) {
// Minifiy it, to make all the keys in the subnetMap relative to their original base network
// Then expand it, but with the new CIDR as the base network, effectively converting from old to new.
let miniSubnetMap = {}
minifySubnetMap(miniSubnetMap, subnetMap, Object.keys(subnetMap)[0])
let newSubnetMap = {}
expandSubnetMap(newSubnetMap, miniSubnetMap, newBaseNetwork)
return newSubnetMap
}
function isMatchingSize(subnet1, subnet2) {
return subnet1.split('/')[1] === subnet2.split('/')[1];
}
$('#calcbody').on('click', 'td.split,td.join', function(event) { $('#calcbody').on('click', 'td.split,td.join', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags // HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '') mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '')
@ -228,15 +276,16 @@ function addRow(network, netSize, colspan, note, notesWidth, color, operatingMod
rangeCol = int2ip(addressFirst); rangeCol = int2ip(addressFirst);
usableCol = int2ip(usableFirst); usableCol = int2ip(usableFirst);
} }
let rowId = 'row_' + network.replace('.', '-') + '_' + netSize
let rowCIDR = network + '/' + netSize
let newRow = let newRow =
' <tr id="row_' + network.replace('.', '-') + '_' + netSize + '"' + styleTag + '>\n' + ' <tr id="' + rowId + '"' + styleTag + ' aria-label="' + rowCIDR + '">\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_address">' + network + '/' + netSize + '</td>\n' + ' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' subnetHeader" class="row_address">' + rowCIDR + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_range">' + rangeCol + '</td>\n' + ' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' rangeHeader" class="row_range">' + rangeCol + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_usable">' + usableCol + '</td>\n' + ' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' useableHeader" class="row_usable">' + usableCol + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_hosts">' + hostCount + '</td>\n' + ' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' hostsHeader" class="row_hosts">' + hostCount + '</td>\n' +
' <td class="note" style="width:' + notesWidth + '"><label><input type="text" class="form-control shadow-none p-0" data-subnet="' + network + '/' + netSize + '" value="' + note + '"></label></td>\n' + ' <td class="note" style="width:' + notesWidth + '"><label><input aria-labelledby="' + rowId + ' noteHeader" type="text" class="form-control shadow-none p-0" data-subnet="' + rowCIDR + '" value="' + note + '"></label></td>\n' +
' <td rowspan="1" colspan="' + colspan + '" class="split rotate" data-subnet="' + network + '/' + netSize + '" data-mutate-verb="split"><span>/' + netSize + '</span></td>\n' ' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' splitHeader" rowspan="1" colspan="' + colspan + '" class="split rotate" data-mutate-verb="split"><span>/' + netSize + '</span></td>\n'
if (netSize > maxNetSize) { if (netSize > maxNetSize) {
// This is wrong. Need to figure out a way to get the number of children so you can set rowspan and the number // This is wrong. Need to figure out a way to get the number of children so you can set rowspan and the number
// of ancestors so you can set colspan. // of ancestors so you can set colspan.
@ -247,7 +296,7 @@ function addRow(network, netSize, colspan, note, notesWidth, color, operatingMod
for (const i in matchingNetworkList) { for (const i in matchingNetworkList) {
let matchingNetwork = matchingNetworkList[i] let matchingNetwork = matchingNetworkList[i]
let networkChildrenCount = count_network_children(matchingNetwork, subnetMap, []) let networkChildrenCount = count_network_children(matchingNetwork, subnetMap, [])
newRow += ' <td rowspan="' + networkChildrenCount + '" colspan="1" class="join rotate" data-subnet="' + matchingNetwork + '" data-mutate-verb="join"><span>/' + matchingNetwork.split('/')[1] + '</span></td>\n' newRow += ' <td aria-label="' + matchingNetwork + ' Join" rowspan="' + networkChildrenCount + '" colspan="1" class="join rotate" data-subnet="' + matchingNetwork + '" data-mutate-verb="join"><span>/' + matchingNetwork.split('/')[1] + '</span></td>\n'
} }
} }
newRow += ' </tr>'; newRow += ' </tr>';
@ -262,7 +311,93 @@ function ip2int(ip) {
} }
function int2ip (ipInt) { function int2ip (ipInt) {
return ( (ipInt>>>24) +'.' + (ipInt>>16 & 255) +'.' + (ipInt>>8 & 255) +'.' + (ipInt & 255) ); return ((ipInt>>>24) + '.' + (ipInt>>16 & 255) + '.' + (ipInt>>8 & 255) + '.' + (ipInt & 255));
}
function toBase36(num) {
return num.toString(36);
}
function fromBase36(str) {
return parseInt(str, 36);
}
/**
* Coordinate System for Subnet Representation
*
* This system aims to represent subnets efficiently within a larger network space.
* The goal is to produce the shortest possible string representation for subnets,
* which is particularly effective when dealing with hierarchical network designs.
*
* Key concept:
* - We represent a subnet by its ordinal position within a larger network,
* along with its mask size.
* - This approach is most efficient when subnets are relatively close together
* in the address space and of similar sizes.
*
* Benefits:
* 1. Compact representation: Often results in very short strings (e.g., "7k").
* 2. Hierarchical: Naturally represents subnet hierarchy.
* 3. Efficient for common cases: Works best for typical network designs where
* subnets are grouped and of similar sizes.
*
* Trade-offs:
* - Less efficient for representing widely dispersed or highly varied subnet sizes.
* - Requires knowledge of the base network to interpret.
*
* Extreme Example... Representing the value 192.168.200.210/31 within the base
* network of 192.168.200.192/27. These are arbitrary but long subnets to represent
* as a string.
* - Normal Way - '192.168.200.210/31'
* - Nth Position Way - '9v'
* - '9' represents the 9th /31 subnet within the /27
* - 'v' represents the /31 mask size converted to Base 36 (31 -> 'v')
*/
/**
* Converts a specific subnet to its Nth position representation within a base network.
*
* @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16")
* @param {string} specificSubnet - The subnet to be represented (e.g., "10.0.112.0/20")
* @returns {string} A compact string representing the subnet's position and size (e.g., "7k")
*/
function getNthSubnet(baseNetwork, specificSubnet) {
const [baseIp, baseMask] = baseNetwork.split('/');
const [specificIp, specificMask] = specificSubnet.split('/');
const baseInt = ip2int(baseIp);
const specificInt = ip2int(specificIp);
const baseSize = 32 - parseInt(baseMask, 10);
const specificSize = 32 - parseInt(specificMask, 10);
const offset = specificInt - baseInt;
const nthSubnet = offset >>> specificSize;
return `${nthSubnet}${toBase36(parseInt(specificMask, 10))}`;
}
/**
* Reconstructs a subnet from its Nth position representation within a base network.
*
* @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16")
* @param {string} nthString - The compact representation of the subnet (e.g., "7k")
* @returns {string} The full subnet representation (e.g., "10.0.112.0/20")
*/
// Takes 10.0.0.0/16 and '7k' and returns 10.0.96.0/20
// '10.0.96.0/20' being the 7th /20 (base36 'k' is 20 int) within the /16.
function getSubnetFromNth(baseNetwork, nthString) {
const [baseIp, baseMask] = baseNetwork.split('/');
const baseInt = ip2int(baseIp);
const size = fromBase36(nthString.slice(-1));
const nth = parseInt(nthString.slice(0, -1), 10);
const innerSizeInt = 32 - size;
const subnetInt = baseInt + (nth << innerSizeInt);
return `${int2ip(subnetInt)}/${size}`;
} }
function subnet_last_address(subnet, netSize) { function subnet_last_address(subnet, netSize) {
@ -320,7 +455,7 @@ function has_network_sub_keys(dict) {
let allKeys = Object.keys(dict) let allKeys = Object.keys(dict)
// Maybe an efficient way to do this with a Lambda? // Maybe an efficient way to do this with a Lambda?
for (let i in allKeys) { for (let i in allKeys) {
if (!allKeys[i].startsWith('_')) { if (!allKeys[i].startsWith('_') && allKeys[i] !== 'n' && allKeys[i] !== 'c') {
return true return true
} }
} }
@ -612,25 +747,25 @@ $( document ).ready(function() {
} }
}, },
errorPlacement: function(error, element) { errorPlacement: function(error, element) {
console.log(error); //console.log(error);
console.log(element); //console.log(element);
if (error[0].innerHTML !== '') { if (error[0].innerHTML !== '') {
console.log('Error Placement - Text') //console.log('Error Placement - Text')
if (!element.data('errorIsVisible')) { if (!element.data('errorIsVisible')) {
bootstrap.Tooltip.getInstance(element).setContent({'.tooltip-inner': error[0].innerHTML}) bootstrap.Tooltip.getInstance(element).setContent({'.tooltip-inner': error[0].innerHTML})
element.tooltip('show'); element.tooltip('show');
element.data('errorIsVisible', true) element.data('errorIsVisible', true)
} }
} else { } else {
console.log('Error Placement - Empty') //console.log('Error Placement - Empty')
console.log(element); //console.log(element);
if (element.data('errorIsVisible')) { if (element.data('errorIsVisible')) {
element.tooltip('hide'); element.tooltip('hide');
element.data('errorIsVisible', false) element.data('errorIsVisible', false)
} }
} }
console.log(element); //console.log(element);
}, },
// This success function appears to be required as errorPlacement() does not fire without the success function // This success function appears to be required as errorPlacement() does not fire without the success function
// being defined. // being defined.
@ -646,33 +781,40 @@ $( document ).ready(function() {
if (!autoConfigResult) { if (!autoConfigResult) {
reset(); reset();
} }
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{},"10.0.64.0/18":{}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{},"10.0.164.0/22":{}},"10.0.168.0/21":{}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{},"10.0.178.0/23":{}},"10.0.180.0/22":{}},"10.0.184.0/21":{}}}},"10.0.192.0/18":{"10.0.192.0/19":{},"10.0.224.0/19":{}}}}},"notes":{}}')
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{"_note":"Note 1"},"10.0.64.0/18":{"_note":"Note 2"}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{"_note":"Note 3"},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{"_note":"Note 4"},"10.0.164.0/22":{"_note":"Note 5"}},"10.0.168.0/21":{"_note":"Note 6"}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{"_note":"Note 7"},"10.0.178.0/23":{"_note":"Note 8"}},"10.0.180.0/22":{"_note":"Note 9"}},"10.0.184.0/21":{"_note":"Note 10"}}}},"10.0.192.0/18":{"10.0.192.0/19":{"_note":"Note 11"},"10.0.224.0/19":{"_note":"Note 12"}}}}},"notes":{}}')
}); });
function exportConfig() { function exportConfig(isMinified = true) {
const baseNetwork = Object.keys(subnetMap)[0]
let miniSubnetMap = {};
if (isMinified) {
minifySubnetMap(miniSubnetMap, subnetMap, baseNetwork)
}
if (operatingMode !== 'Standard') { if (operatingMode !== 'Standard') {
return { return {
'config_version': configVersion, 'config_version': configVersion,
'operating_mode': operatingMode, 'operating_mode': operatingMode,
'subnets': subnetMap, 'base_network': baseNetwork,
'subnets': isMinified ? miniSubnetMap : subnetMap,
} }
} else { } else {
return { return {
'config_version': configVersion, 'config_version': configVersion,
'subnets': subnetMap, 'base_network': baseNetwork,
'subnets': isMinified ? miniSubnetMap : subnetMap,
} }
} }
} }
function getConfigUrl() { function getConfigUrl() {
let defaultExport = JSON.parse(JSON.stringify(exportConfig())); // Deep Copy
let defaultExport = JSON.parse(JSON.stringify(exportConfig(true)));
renameKey(defaultExport, 'config_version', 'v') renameKey(defaultExport, 'config_version', 'v')
renameKey(defaultExport, 'base_network', 'b')
if (defaultExport.hasOwnProperty('operating_mode')) { if (defaultExport.hasOwnProperty('operating_mode')) {
renameKey(defaultExport, 'operating_mode', 'm') renameKey(defaultExport, 'operating_mode', 'm')
} }
renameKey(defaultExport, 'subnets', 's') renameKey(defaultExport, 'subnets', 's')
shortenKeys(defaultExport['s']) //console.log(JSON.stringify(defaultExport))
return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport)) return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport))
} }
@ -690,31 +832,63 @@ function processConfigUrl() {
renameKey(urlConfig, 'm', 'operating_mode') renameKey(urlConfig, 'm', 'operating_mode')
} }
renameKey(urlConfig, 's', 'subnets') renameKey(urlConfig, 's', 'subnets')
expandKeys(urlConfig['subnets']) if (urlConfig['config_version'] === '1') {
// Version 1 Configs used full subnet strings as keys and just shortned the _note->_n and _color->_c keys
expandKeys(urlConfig['subnets'])
} else if (urlConfig['config_version'] === '2') {
// Version 2 Configs uses the Nth Position representation for subnet keys and requires the base_network
// option. It also uses n/c for note/color
if (urlConfig.hasOwnProperty('b')) {
renameKey(urlConfig, 'b', 'base_network')
}
let expandedSubnetMap = {};
expandSubnetMap(expandedSubnetMap, urlConfig['subnets'], urlConfig['base_network'])
urlConfig['subnets'] = expandedSubnetMap
}
importConfig(urlConfig) importConfig(urlConfig)
return true return true
} }
} }
function shortenKeys(subnetTree) { function minifySubnetMap(minifiedMap, referenceMap, baseNetwork) {
for (let mapKey in subnetTree) { for (let subnet in referenceMap) {
if (mapKey.startsWith('_')) { if (subnet.startsWith('_')) continue;
continue;
}
if (has_network_sub_keys(subnetTree[mapKey])) {
shortenKeys(subnetTree[mapKey])
} else {
if (subnetTree[mapKey].hasOwnProperty('_note')) {
renameKey(subnetTree[mapKey], '_note', '_n')
}
if (subnetTree[mapKey].hasOwnProperty('_color')) {
renameKey(subnetTree[mapKey], '_color', '_c')
}
const nthRepresentation = getNthSubnet(baseNetwork, subnet);
minifiedMap[nthRepresentation] = {}
if (referenceMap[subnet].hasOwnProperty('_note')) {
minifiedMap[nthRepresentation]['n'] = referenceMap[subnet]['_note']
}
if (referenceMap[subnet].hasOwnProperty('_color')) {
minifiedMap[nthRepresentation]['c'] = referenceMap[subnet]['_color']
}
if (Object.keys(referenceMap[subnet]).some(key => !key.startsWith('_'))) {
minifySubnetMap(minifiedMap[nthRepresentation], referenceMap[subnet], baseNetwork);
} }
} }
} }
function expandSubnetMap(expandedMap, miniMap, baseNetwork) {
for (let mapKey in miniMap) {
if (mapKey === 'n' || mapKey === 'c') {
continue;
}
let subnetKey = getSubnetFromNth(baseNetwork, mapKey)
expandedMap[subnetKey] = {}
if (has_network_sub_keys(miniMap[mapKey])) {
expandSubnetMap(expandedMap[subnetKey], miniMap[mapKey], baseNetwork)
} else {
if (miniMap[mapKey].hasOwnProperty('n')) {
expandedMap[subnetKey]['_note'] = miniMap[mapKey]['n']
}
if (miniMap[mapKey].hasOwnProperty('c')) {
expandedMap[subnetKey]['_color'] = miniMap[mapKey]['c']
}
}
}
}
// For Config Version 1 Backwards Compatibility
function expandKeys(subnetTree) { function expandKeys(subnetTree) {
for (let mapKey in subnetTree) { for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { if (mapKey.startsWith('_')) {
@ -734,7 +908,6 @@ function expandKeys(subnetTree) {
} }
} }
function renameKey(obj, oldKey, newKey) { function renameKey(obj, oldKey, newKey) {
if (oldKey !== newKey) { if (oldKey !== newKey) {
Object.defineProperty(obj, newKey, Object.defineProperty(obj, newKey,
@ -745,13 +918,16 @@ function renameKey(obj, oldKey, newKey) {
function importConfig(text) { function importConfig(text) {
if (text['config_version'] === '1') { if (text['config_version'] === '1') {
let subnet_split = Object.keys(text['subnets'])[0].split('/') var [subnetNet, subnetSize] = Object.keys(text['subnets'])[0].split('/')
$('#network').val(subnet_split[0]) } else if (text['config_version'] === '2') {
$('#netsize').val(subnet_split[1]) var [subnetNet, subnetSize] = text['base_network'].split('/')
subnetMap = text['subnets'];
operatingMode = text['operating_mode'] || 'Standard'
switchMode(operatingMode);
} }
$('#network').val(subnetNet)
$('#netsize').val(subnetSize)
subnetMap = text['subnets'];
operatingMode = text['operating_mode'] || 'Standard'
switchMode(operatingMode);
} }
const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}` const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`

19
repopack.config.json Normal file
View file

@ -0,0 +1,19 @@
{
"output": {
"filePath": "repopack-output.txt",
"style": "xml",
"removeComments": false,
"removeEmptyLines": false,
"topFilesLength": 5,
"showLineNumbers": false
},
"include": [],
"ignore": {
"useGitignore": true,
"useDefaultPatterns": false,
"customPatterns": []
},
"security": {
"enableSecurityCheck": true
}
}

5
src/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

1121
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,19 @@
{ {
"dependencies": { "dependencies": {
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"lz-string": "^1.5.0" "lz-string": "^1.5.0"
}, },
"scripts": { "scripts": {
"postinstall": "sudo npm install -g sass", "postinstall": "sudo npm install -g sass@1.77.6",
"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", "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", "test": "npx playwright test",
"start": "node node_modules/http-server/bin/http-server ../dist -c-1", "setup:certs": "mkdir -p certs; mkcert -cert-file certs/cert.pem -key-file certs/cert.key localhost 127.0.0.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" "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"
}
} }

99
src/playwright.config.ts Normal file
View file

@ -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: 'https://localhost:8443',
ignoreHTTPSErrors: true,
/* 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 build && npm run local-secure-start',
port: 8443,
reuseExistingServer: !process.env.CI,
},
});

2
src/tests/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# This file contains a private dataset used for testing real world use cases
real-world-functional.spec.ts

View file

@ -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('/');
// 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('/');
//});

View file

@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';
test('Default Homepage Rendering', async ({ page }) => {
await page.goto('/');
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');
});

View file

@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
test('Default Export Content', async ({ page }) => {
await page.goto('/');
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('/');
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('/');
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('/');
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('/');
//});

Binary file not shown.

View file

@ -0,0 +1,174 @@
import { test, expect } from '@playwright/test';
async function getClipboardText(page) {
return page.evaluate(async () => {
return await navigator.clipboard.readText();
});
}
test('Renders Max Depth /0 to /32', async ({ page }) => {
await page.goto('/');
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('/');
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('/');
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('/');
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('/');
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('/');
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('/');
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('/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('/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('/');
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('/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('/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('/');
//});

View file

@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';
test('Bad Network Address', async ({ page }) => {
await page.goto('/');
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('/');
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('/');
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('/');
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('/');
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('/');
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();
});

168
src/tests/ui-usage.spec.ts Normal file
View file

@ -0,0 +1,168 @@
import { test, expect } from '@playwright/test';
test('CIDR Input Typing', async ({ page }) => {
await page.goto('/');
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('/');
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('/');
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('/');
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('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
});
test('Table Header AWS Mode', async ({ page }) => {
await page.goto('/');
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('/');
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('/');
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('/');
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('/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)');
});

View file

@ -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('/');
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('/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('/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('/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('/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('/');
//});