mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-03 08:00:34 +00:00
Compare commits
172 Commits
v0.2.6
...
wpt-faster
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc2b0edb5 | ||
|
|
3e7ddf8146 | ||
|
|
a865b86fa5 | ||
|
|
de28d14aff | ||
|
|
4cdc24326a | ||
|
|
cf46f0097a | ||
|
|
d94fd2a43b | ||
|
|
8c5e737669 | ||
|
|
fb29a1c5bf | ||
|
|
94190f93af | ||
|
|
93e239f682 | ||
|
|
a2e59af44c | ||
|
|
00c962bdd8 | ||
|
|
1fa87442b8 | ||
|
|
ac5400696a | ||
|
|
5062273b7a | ||
|
|
9c2393351d | ||
|
|
f0cfe3ffc8 | ||
|
|
615fcffb99 | ||
|
|
13b746f9e4 | ||
|
|
e90fce4c55 | ||
|
|
59175437b5 | ||
|
|
e950384b9b | ||
|
|
78440350dc | ||
|
|
f435297949 | ||
|
|
54d1563cf3 | ||
|
|
f36499b806 | ||
|
|
fa1dd5237d | ||
|
|
2b9d5fd4d9 | ||
|
|
964fa0a8aa | ||
|
|
db01158d2d | ||
|
|
e997f8317e | ||
|
|
a88c21cdb5 | ||
|
|
7a7c4b9f49 | ||
|
|
edd0c5c83f | ||
|
|
c6861829c3 | ||
|
|
e14c8b3025 | ||
|
|
5bc00c595c | ||
|
|
db5fb40de0 | ||
|
|
4e6a357e6e | ||
|
|
6cf515151d | ||
|
|
bf6e4cf3a6 | ||
|
|
60936baa96 | ||
|
|
c29f72a7e8 | ||
|
|
d4427e4370 | ||
|
|
b85ec04175 | ||
|
|
da05ba0eb7 | ||
|
|
414a68abeb | ||
|
|
52455b732b | ||
|
|
ba71268eb3 | ||
|
|
694aac5ce8 | ||
|
|
cbab0b712a | ||
|
|
1aee3db521 | ||
|
|
f634c9843d | ||
|
|
e1e45d1c5d | ||
|
|
ff288c8aa2 | ||
|
|
e1b14a6833 | ||
|
|
015edc3848 | ||
|
|
bd2406f803 | ||
|
|
3c29e7dbd4 | ||
|
|
586413357e | ||
|
|
9a055a61a6 | ||
|
|
5fb561dc9c | ||
|
|
b14ae02548 | ||
|
|
51fb08e6aa | ||
|
|
a6d699ad5d | ||
|
|
8372b45cc5 | ||
|
|
1739ae6b9a | ||
|
|
ba62150f7a | ||
|
|
8143a61955 | ||
|
|
e16c479781 | ||
|
|
c0c4e26d63 | ||
|
|
b252aa71d0 | ||
|
|
9ef8d9c189 | ||
|
|
9f27416603 | ||
|
|
0729f4a03a | ||
|
|
21f7b95db9 | ||
|
|
4125a5aa1e | ||
|
|
6d0dc6cb1e | ||
|
|
0675c23217 | ||
|
|
d0e6a1f5bb | ||
|
|
91afe08235 | ||
|
|
041d9d41fb | ||
|
|
7009fb5899 | ||
|
|
d2003c7c9a | ||
|
|
ce002b999c | ||
|
|
5b1056862a | ||
|
|
cc4ac99b4a | ||
|
|
46df341506 | ||
|
|
b698e2d078 | ||
|
|
5cc5e513dd | ||
|
|
e048b0372f | ||
|
|
d7aaa1c870 | ||
|
|
463aac9b59 | ||
|
|
d9cdd78138 | ||
|
|
44a83c0e1c | ||
|
|
96f24a2662 | ||
|
|
5d2801c652 | ||
|
|
deb08b7880 | ||
|
|
96e5054ffc | ||
|
|
c9753a690d | ||
|
|
27aaf46630 | ||
|
|
84190e1e06 | ||
|
|
b0b1f755ea | ||
|
|
fcf1d30c77 | ||
|
|
3c532e5aef | ||
|
|
3efcb2705d | ||
|
|
c25f389e91 | ||
|
|
533f4075a3 | ||
|
|
f508d37426 | ||
|
|
548c6eeb7a | ||
|
|
c8265f4807 | ||
|
|
a74e46debf | ||
|
|
1ceaabe69f | ||
|
|
91a2441ed8 | ||
|
|
2ecbc833a9 | ||
|
|
dac456d98c | ||
|
|
422320d9ac | ||
|
|
18b635936c | ||
|
|
7b2895ef08 | ||
|
|
b09e9f7398 | ||
|
|
ac651328c3 | ||
|
|
0380df1cb4 | ||
|
|
21421d5b53 | ||
|
|
80c309aa69 | ||
|
|
f5bc7310b1 | ||
|
|
21e9967a8a | ||
|
|
32f450f803 | ||
|
|
1972142703 | ||
|
|
b10d866e4b | ||
|
|
b373fb4a42 | ||
|
|
ddd34dc57b | ||
|
|
265c5aba2e | ||
|
|
21fc6d1cf6 | ||
|
|
1a7fe6129c | ||
|
|
37462a16c5 | ||
|
|
323ec0046c | ||
|
|
dc7c6984fb | ||
|
|
92f7248a16 | ||
|
|
1ec3e156fb | ||
|
|
1121bed49b | ||
|
|
0eb43fb530 | ||
|
|
1f50dc38c3 | ||
|
|
a9d044ec10 | ||
|
|
1bdf464ef2 | ||
|
|
a70da0d176 | ||
|
|
8c52b8357c | ||
|
|
0243c6b450 | ||
|
|
f7071447cb | ||
|
|
c038bfafa1 | ||
|
|
4d60f56e66 | ||
|
|
56d3cf51e8 | ||
|
|
3013e3a9e6 | ||
|
|
fe9b2e672b | ||
|
|
3e9fa4ca47 | ||
|
|
a2e66f85a1 | ||
|
|
a9b9cf14c3 | ||
|
|
d4b941cf30 | ||
|
|
4b6bf29b83 | ||
|
|
a8b147dfc0 | ||
|
|
65627c1296 | ||
|
|
3dcdaa0a9b | ||
|
|
5bc00045c7 | ||
|
|
93ea95af24 | ||
|
|
f754773bf6 | ||
|
|
42bb2f3c58 | ||
|
|
68337a6989 | ||
|
|
bf6dbedbe4 | ||
|
|
a204f40968 | ||
|
|
1352839472 | ||
|
|
099550dddc | ||
|
|
8b310ce993 |
4
.github/actions/install/action.yml
vendored
4
.github/actions/install/action.yml
vendored
@@ -13,7 +13,7 @@ inputs:
|
|||||||
zig-v8:
|
zig-v8:
|
||||||
description: 'zig v8 version to install'
|
description: 'zig v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
default: 'v0.3.3'
|
default: 'v0.3.4'
|
||||||
v8:
|
v8:
|
||||||
description: 'v8 version to install'
|
description: 'v8 version to install'
|
||||||
required: false
|
required: false
|
||||||
@@ -46,7 +46,7 @@ runs:
|
|||||||
|
|
||||||
- name: Cache v8
|
- name: Cache v8
|
||||||
id: cache-v8
|
id: cache-v8
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
env:
|
env:
|
||||||
cache-name: cache-v8
|
cache-name: cache-v8
|
||||||
with:
|
with:
|
||||||
|
|||||||
10
.github/workflows/e2e-integration-test.yml
vendored
10
.github/workflows/e2e-integration-test.yml
vendored
@@ -20,11 +20,9 @@ jobs:
|
|||||||
if: github.event.pull_request.draft == false
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -47,7 +45,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -55,7 +53,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
|||||||
69
.github/workflows/e2e-test.yml
vendored
69
.github/workflows/e2e-test.yml
vendored
@@ -9,15 +9,13 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# By default GH trigger on types opened, synchronize and reopened.
|
||||||
@@ -29,12 +27,10 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -52,8 +48,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -61,7 +55,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -76,7 +70,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -84,7 +78,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -126,7 +120,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -134,7 +128,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -182,39 +176,41 @@ jobs:
|
|||||||
name: wba-test
|
name: wba-test
|
||||||
needs: zig-build-release
|
needs: zig-build-release
|
||||||
|
|
||||||
env:
|
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- run: echo "${{ secrets.WBA_PRIVATE_KEY_PEM }}" > private_key.pem
|
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
|
# force a wakup of the auth server before requesting it w/ the test itself
|
||||||
|
- run: curl https://${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
- name: run wba test
|
- name: run wba test
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
node webbotauth/validator.js &
|
node webbotauth/validator.js &
|
||||||
VALIDATOR_PID=$!
|
VALIDATOR_PID=$!
|
||||||
sleep 2
|
sleep 5
|
||||||
|
|
||||||
./lightpanda fetch http://127.0.0.1:8989/ \
|
exec 3<<< "${{ secrets.WBA_PRIVATE_KEY_PEM }}"
|
||||||
--web_bot_auth_key_file private_key.pem \
|
|
||||||
|
./lightpanda fetch --dump http://127.0.0.1:8989/ \
|
||||||
|
--web_bot_auth_key_file /proc/self/fd/3 \
|
||||||
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
--web_bot_auth_keyid ${{ vars.WBA_KEY_ID }} \
|
||||||
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
--web_bot_auth_domain ${{ vars.WBA_DOMAIN }}
|
||||||
|
|
||||||
wait $VALIDATOR_PID
|
wait $VALIDATOR_PID
|
||||||
|
exec 3>&-
|
||||||
|
|
||||||
cdp-and-hyperfine-bench:
|
cdp-and-hyperfine-bench:
|
||||||
name: cdp-and-hyperfine-bench
|
name: cdp-and-hyperfine-bench
|
||||||
@@ -224,7 +220,6 @@ jobs:
|
|||||||
MAX_VmHWM: 28000 # 28MB (KB)
|
MAX_VmHWM: 28000 # 28MB (KB)
|
||||||
MAX_CG_PEAK: 8000 # 8MB (KB)
|
MAX_CG_PEAK: 8000 # 8MB (KB)
|
||||||
MAX_AVG_DURATION: 17
|
MAX_AVG_DURATION: 17
|
||||||
LIGHTPANDA_DISABLE_TELEMETRY: true
|
|
||||||
|
|
||||||
# How to give cgroups access to the user actions-runner on the host:
|
# How to give cgroups access to the user actions-runner on the host:
|
||||||
# $ sudo apt install cgroup-tools
|
# $ sudo apt install cgroup-tools
|
||||||
@@ -239,7 +234,7 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: 'lightpanda-io/demo'
|
repository: 'lightpanda-io/demo'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -247,7 +242,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
@@ -333,7 +328,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -356,12 +351,12 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
@@ -379,7 +374,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.NIGHTLY_BUILD_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.NIGHTLY_BUILD_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
AWS_REGION: ${{ vars.NIGHTLY_BUILD_AWS_REGION }}
|
||||||
|
|
||||||
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
RELEASE: ${{ github.ref_type == 'tag' && github.ref_name || 'nightly' }}
|
||||||
|
GIT_VERSION_FLAG: ${{ github.ref_type == 'tag' && format('-Dgit_version={0}', github.ref_name) || '' }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -33,8 +35,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -72,11 +72,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -87,7 +85,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -116,11 +114,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -131,7 +127,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
@@ -158,11 +154,9 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -173,7 +167,7 @@ jobs:
|
|||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build
|
- name: zig build
|
||||||
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dgit_commit=$(git rev-parse --short ${{ github.sha }}) ${{ env.GIT_VERSION_FLAG }}
|
||||||
|
|
||||||
- name: Rename binary
|
- name: Rename binary
|
||||||
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }}
|
||||||
44
.github/workflows/wpt.yml
vendored
44
.github/workflows/wpt.yml
vendored
@@ -10,32 +10,42 @@ env:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "23 2 * * *"
|
- cron: "21 2 * * *"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wpt-build-release:
|
wpt-build-release:
|
||||||
name: zig build release
|
name: zig build release
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
env:
|
||||||
timeout-minutes: 15
|
ARCH: aarch64
|
||||||
|
OS: linux
|
||||||
|
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
with:
|
||||||
|
os: ${{env.OS}}
|
||||||
|
arch: ${{env.ARCH}}
|
||||||
|
|
||||||
|
- name: v8 snapshot
|
||||||
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin
|
||||||
|
|
||||||
- name: zig build release
|
- name: zig build release
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic -Dgit_commit=$(git rev-parse --short ${{ github.sha }})
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
path: |
|
path: |
|
||||||
@@ -45,7 +55,7 @@ jobs:
|
|||||||
wpt-build-runner:
|
wpt-build-runner:
|
||||||
name: build wpt runner
|
name: build wpt runner
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04-arm
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -59,7 +69,7 @@ jobs:
|
|||||||
CGO_ENABLED=0 go build
|
CGO_ENABLED=0 go build
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wptrunner
|
name: wptrunner
|
||||||
path: |
|
path: |
|
||||||
@@ -73,8 +83,8 @@ jobs:
|
|||||||
- wpt-build-runner
|
- wpt-build-runner
|
||||||
|
|
||||||
# use a self host runner.
|
# use a self host runner.
|
||||||
runs-on: lpd-bench-hetzner
|
runs-on: lpd-wpt-aws
|
||||||
timeout-minutes: 180
|
timeout-minutes: 600
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
@@ -91,14 +101,14 @@ jobs:
|
|||||||
run: ./wpt manifest
|
run: ./wpt manifest
|
||||||
|
|
||||||
- name: download lightpanda release
|
- name: download lightpanda release
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: lightpanda-build-release
|
name: lightpanda-build-release
|
||||||
|
|
||||||
- run: chmod a+x ./lightpanda
|
- run: chmod a+x ./lightpanda
|
||||||
|
|
||||||
- name: download wptrunner
|
- name: download wptrunner
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: wptrunner
|
name: wptrunner
|
||||||
|
|
||||||
@@ -107,8 +117,8 @@ jobs:
|
|||||||
- name: run test with json output
|
- name: run test with json output
|
||||||
run: |
|
run: |
|
||||||
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
./wpt serve 2> /dev/null & echo $! > WPT.pid
|
||||||
sleep 10s
|
sleep 20s
|
||||||
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 3 > wpt.json
|
./wptrunner -lpd-path ./lightpanda -json -concurrency 10 -pool 10 --mem-limit 400 > wpt.json
|
||||||
kill `cat WPT.pid`
|
kill `cat WPT.pid`
|
||||||
|
|
||||||
- name: write commit
|
- name: write commit
|
||||||
@@ -116,7 +126,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
path: |
|
path: |
|
||||||
@@ -139,7 +149,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: wpt-results
|
name: wpt-results
|
||||||
|
|
||||||
|
|||||||
60
.github/workflows/zig-fmt.yml
vendored
60
.github/workflows/zig-fmt.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: zig-fmt
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
|
||||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
|
||||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
|
||||||
# running when the PR is marked ready_for_review w/o other change.
|
|
||||||
# see https://github.com/orgs/community/discussions/25722#discussioncomment-3248917
|
|
||||||
types: [opened, synchronize, reopened, ready_for_review]
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- ".github/**"
|
|
||||||
- "build.zig"
|
|
||||||
- "src/**/*.zig"
|
|
||||||
- "src/*.zig"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zig-fmt:
|
|
||||||
name: zig fmt
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
|
||||||
- uses: mlugg/setup-zig@v2
|
|
||||||
|
|
||||||
- name: Run zig fmt
|
|
||||||
id: fmt
|
|
||||||
run: |
|
|
||||||
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
|
||||||
delimiter="$(openssl rand -hex 8)"
|
|
||||||
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err ]; then
|
|
||||||
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -s zig-fmt.err2 ]; then
|
|
||||||
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
|
||||||
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Fail the job
|
|
||||||
if: steps.fmt.outputs.zig_fmt_errs != ''
|
|
||||||
run: exit 1
|
|
||||||
98
.github/workflows/zig-test.yml
vendored
98
.github/workflows/zig-test.yml
vendored
@@ -5,19 +5,18 @@ env:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.LPD_PERF_AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
AWS_BUCKET: ${{ vars.LPD_PERF_AWS_BUCKET }}
|
||||||
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
AWS_REGION: ${{ vars.LPD_PERF_AWS_REGION }}
|
||||||
|
LIGHTPANDA_DISABLE_TELEMETRY: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
|
||||||
paths:
|
paths:
|
||||||
- "build.zig"
|
|
||||||
- "src/**"
|
|
||||||
- "vendor/zig-js-runtime"
|
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "vendor/**"
|
- "src/**"
|
||||||
pull_request:
|
- "build.zig"
|
||||||
|
- "build.zig.zon"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
# By default GH trigger on types opened, synchronize and reopened.
|
# By default GH trigger on types opened, synchronize and reopened.
|
||||||
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
# see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||||
# Since we skip the job when the PR is in draft state, we want to force CI
|
# Since we skip the job when the PR is in draft state, we want to force CI
|
||||||
@@ -27,28 +26,63 @@ on:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
- ".github/**"
|
- ".github/**"
|
||||||
|
- "src/**"
|
||||||
- "build.zig"
|
- "build.zig"
|
||||||
- "src/**/*.zig"
|
- "build.zig.zon"
|
||||||
- "src/*.zig"
|
|
||||||
- "vendor/**"
|
|
||||||
- ".github/**"
|
|
||||||
- "vendor/**"
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zig-test-debug:
|
zig-fmt:
|
||||||
name: zig test using v8 in debug mode
|
name: zig fmt
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
# Zig version used from the `minimum_zig_version` field in build.zig.zon
|
||||||
|
- uses: mlugg/setup-zig@v2
|
||||||
|
|
||||||
|
- name: Run zig fmt
|
||||||
|
id: fmt
|
||||||
|
run: |
|
||||||
|
zig fmt --check ./*.zig ./**/*.zig 2> zig-fmt.err > zig-fmt.err2 || echo "Failed"
|
||||||
|
delimiter="$(openssl rand -hex 8)"
|
||||||
|
echo "zig_fmt_errs<<${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err ]; then
|
||||||
|
echo "// The following errors occurred:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s zig-fmt.err2 ]; then
|
||||||
|
echo "// The following files were not formatted:" >> "${GITHUB_OUTPUT}"
|
||||||
|
cat zig-fmt.err2 >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${delimiter}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Fail the job
|
||||||
|
if: steps.fmt.outputs.zig_fmt_errs != ''
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
zig-test-debug:
|
||||||
|
name: zig test using v8 in debug mode
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
with:
|
with:
|
||||||
@@ -57,21 +91,18 @@ jobs:
|
|||||||
- name: zig build test
|
- name: zig build test
|
||||||
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
run: zig build -Dprebuilt_v8_path=v8/libc_v8_debug.a -Dtsan=true test
|
||||||
|
|
||||||
zig-test:
|
zig-test-release:
|
||||||
name: zig test
|
name: zig test
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
# Don't run the CI with draft PR.
|
|
||||||
if: github.event.pull_request.draft == false
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# fetch submodules recusively, to get zig-js-runtime submodules also.
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- uses: ./.github/actions/install
|
- uses: ./.github/actions/install
|
||||||
|
|
||||||
@@ -83,7 +114,7 @@ jobs:
|
|||||||
echo "${{github.sha}}" > commit.txt
|
echo "${{github.sha}}" > commit.txt
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
path: |
|
path: |
|
||||||
@@ -93,23 +124,22 @@ jobs:
|
|||||||
|
|
||||||
bench-fmt:
|
bench-fmt:
|
||||||
name: perf-fmt
|
name: perf-fmt
|
||||||
needs: zig-test
|
needs: zig-test-release
|
||||||
|
|
||||||
# Don't execute on PR
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
image: ghcr.io/lightpanda-io/perf-fmt:latest
|
||||||
credentials:
|
credentials:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: bench-results
|
name: bench-results
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ FROM debian:stable-slim
|
|||||||
ARG MINISIG=0.12
|
ARG MINISIG=0.12
|
||||||
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U
|
||||||
ARG V8=14.0.365.4
|
ARG V8=14.0.365.4
|
||||||
ARG ZIG_V8=v0.3.3
|
ARG ZIG_V8=v0.3.4
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update -yq && \
|
RUN apt-get update -yq && \
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -47,7 +47,7 @@ help:
|
|||||||
|
|
||||||
# $(ZIG) commands
|
# $(ZIG) commands
|
||||||
# ------------
|
# ------------
|
||||||
.PHONY: build build-v8-snapshot build-dev run run-release shell test bench data end2end
|
.PHONY: build build-v8-snapshot build-dev run run-release test bench data end2end
|
||||||
|
|
||||||
## Build v8 snapshot
|
## Build v8 snapshot
|
||||||
build-v8-snapshot:
|
build-v8-snapshot:
|
||||||
@@ -77,11 +77,6 @@ run-debug: build-dev
|
|||||||
@printf "\033[36mRunning...\033[0m\n"
|
@printf "\033[36mRunning...\033[0m\n"
|
||||||
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
@./zig-out/bin/lightpanda || (printf "\033[33mRun ERROR\033[0m\n"; exit 1;)
|
||||||
|
|
||||||
## Run a JS shell in debug mode
|
|
||||||
shell:
|
|
||||||
@printf "\033[36mBuilding shell...\033[0m\n"
|
|
||||||
@$(ZIG) build shell || (printf "\033[33mBuild ERROR\033[0m\n"; exit 1;)
|
|
||||||
|
|
||||||
## Test - `grep` is used to filter out the huge compile command on build
|
## Test - `grep` is used to filter out the huge compile command on build
|
||||||
ifeq ($(OS), macos)
|
ifeq ($(OS), macos)
|
||||||
test:
|
test:
|
||||||
@@ -106,4 +101,3 @@ install: build
|
|||||||
|
|
||||||
data:
|
data:
|
||||||
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
cd src/data && go run public_suffix_list_gen.go > public_suffix_list.zig
|
||||||
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -1,18 +1,32 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
<a href="https://lightpanda.io"><img src="https://cdn.lightpanda.io/assets/images/logo/lpd-logo.png" alt="Logo" height=170></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">Lightpanda Browser</h1>
|
<h1 align="center">Lightpanda Browser</h1>
|
||||||
|
<p align="center">
|
||||||
|
<strong>The headless browser built from scratch for AI agents and automation.</strong><br>
|
||||||
|
Not a Chromium fork. Not a WebKit patch. A new browser, written in Zig.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center"><a href="https://lightpanda.io/">lightpanda.io</a></p>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
[](https://github.com/lightpanda-io/browser/blob/main/LICENSE)
|
||||||
[](https://twitter.com/lightpanda_io)
|
[](https://twitter.com/lightpanda_io)
|
||||||
[](https://github.com/lightpanda-io/browser)
|
[](https://github.com/lightpanda-io/browser)
|
||||||
|
[](https://discord.gg/K63XeymfB5)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
||||||
|
](https://github.com/lightpanda-io/demo)
|
||||||
|
 
|
||||||
|
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
||||||
|
](https://github.com/lightpanda-io/demo)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
||||||
|
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
||||||
|
|
||||||
Lightpanda is the open-source browser made for headless usage:
|
Lightpanda is the open-source browser made for headless usage:
|
||||||
|
|
||||||
@@ -26,16 +40,6 @@ Fast web automation for AI agents, LLM training, scraping and testing:
|
|||||||
- Exceptionally fast execution (11x faster than Chrome)
|
- Exceptionally fast execution (11x faster than Chrome)
|
||||||
- Instant startup
|
- Instant startup
|
||||||
|
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/execution-time.svg">
|
|
||||||
](https://github.com/lightpanda-io/demo)
|
|
||||||
 
|
|
||||||
[<img width="350px" src="https://cdn.lightpanda.io/assets/images/github/memory-frame.svg">
|
|
||||||
](https://github.com/lightpanda-io/demo)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
_Puppeteer requesting 100 pages from a local website on a AWS EC2 m5.large instance.
|
|
||||||
See [benchmark details](https://github.com/lightpanda-io/demo)._
|
|
||||||
|
|
||||||
[^1]: **Playwright support disclaimer:**
|
[^1]: **Playwright support disclaimer:**
|
||||||
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
Due to the nature of Playwright, a script that works with the current version of the browser may not function correctly with a future version. Playwright uses an intermediate JavaScript layer that selects an execution strategy based on the browser's available features. If Lightpanda adds a new [Web API](https://developer.mozilla.org/en-US/docs/Web/API), Playwright may choose to execute different code for the same script. This new code path could attempt to use features that are not yet implemented. Lightpanda makes an effort to add compatibility tests, but we can't cover all scenarios. If you encounter an issue, please create a [GitHub issue](https://github.com/lightpanda-io/browser/issues) and include the last known working version of the script.
|
||||||
|
|
||||||
@@ -186,8 +190,6 @@ Here are the key features we have implemented:
|
|||||||
|
|
||||||
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
NOTE: There are hundreds of Web APIs. Developing a browser (even just for headless mode) is a huge task. Coverage will increase over time.
|
||||||
|
|
||||||
You can also follow the progress of our Javascript support in our dedicated [zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime#development) project.
|
|
||||||
|
|
||||||
## Build from sources
|
## Build from sources
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -196,10 +198,10 @@ Lightpanda is written with [Zig](https://ziglang.org/) `0.15.2`. You have to
|
|||||||
install it with the right version in order to build the project.
|
install it with the right version in order to build the project.
|
||||||
|
|
||||||
Lightpanda also depends on
|
Lightpanda also depends on
|
||||||
[zig-js-runtime](https://github.com/lightpanda-io/zig-js-runtime/) (with v8),
|
[v8](https://chromium.googlesource.com/v8/v8.git),
|
||||||
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
[Libcurl](https://curl.se/libcurl/) and [html5ever](https://github.com/servo/html5ever).
|
||||||
|
|
||||||
To be able to build the v8 engine for zig-js-runtime, you have to install some libs:
|
To be able to build the v8 engine, you have to install some libs:
|
||||||
|
|
||||||
For **Debian/Ubuntu based Linux**:
|
For **Debian/Ubuntu based Linux**:
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ pub fn build(b: *Build) !void {
|
|||||||
const manifest = Manifest.init(b);
|
const manifest = Manifest.init(b);
|
||||||
|
|
||||||
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
const git_commit = b.option([]const u8, "git_commit", "Current git commit");
|
||||||
|
const git_version = b.option([]const u8, "git_version", "Current git version (from tag)");
|
||||||
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
const prebuilt_v8_path = b.option([]const u8, "prebuilt_v8_path", "Path to prebuilt libc_v8.a");
|
||||||
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
const snapshot_path = b.option([]const u8, "snapshot_path", "Path to v8 snapshot");
|
||||||
|
|
||||||
var opts = b.addOptions();
|
var opts = b.addOptions();
|
||||||
opts.addOption([]const u8, "version", manifest.version);
|
opts.addOption([]const u8, "version", manifest.version);
|
||||||
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
opts.addOption([]const u8, "git_commit", git_commit orelse "dev");
|
||||||
|
opts.addOption(?[]const u8, "git_version", git_version orelse null);
|
||||||
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
opts.addOption(?[]const u8, "snapshot_path", snapshot_path);
|
||||||
|
|
||||||
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
const enable_tsan = b.option(bool, "tsan", "Enable Thread Sanitizer") orelse false;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.3.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6yx3BAAGD9jSoq_ttt_bk9MectTU44s_HZxxE5LD",
|
.hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup",
|
||||||
},
|
},
|
||||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.brotli = .{
|
.brotli = .{
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ pub fn init(allocator: Allocator, config: *const Config) !*App {
|
|||||||
app.app_dir_path = getAndMakeAppDir(allocator);
|
app.app_dir_path = getAndMakeAppDir(allocator);
|
||||||
|
|
||||||
app.telemetry = try Telemetry.init(app, config.mode);
|
app.telemetry = try Telemetry.init(app, config.mode);
|
||||||
errdefer app.telemetry.deinit();
|
errdefer app.telemetry.deinit(allocator);
|
||||||
|
|
||||||
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
app.arena_pool = ArenaPool.init(allocator, 512, 1024 * 16);
|
||||||
errdefer app.arena_pool.deinit();
|
errdefer app.arena_pool.deinit();
|
||||||
@@ -85,7 +85,7 @@ pub fn deinit(self: *App) void {
|
|||||||
allocator.free(app_dir_path);
|
allocator.free(app_dir_path);
|
||||||
self.app_dir_path = null;
|
self.app_dir_path = null;
|
||||||
}
|
}
|
||||||
self.telemetry.deinit();
|
self.telemetry.deinit(allocator);
|
||||||
self.network.deinit();
|
self.network.deinit();
|
||||||
self.snapshot.deinit();
|
self.snapshot.deinit();
|
||||||
self.platform.deinit();
|
self.platform.deinit();
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ dom_node: *Node,
|
|||||||
registry: *CDPNode.Registry,
|
registry: *CDPNode.Registry,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
arena: std.mem.Allocator,
|
arena: std.mem.Allocator,
|
||||||
prune: bool = false,
|
prune: bool = true,
|
||||||
|
interactive_only: bool = false,
|
||||||
|
max_depth: u32 = std.math.maxInt(u32) - 1,
|
||||||
|
|
||||||
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!void {
|
||||||
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
var visitor = JsonVisitor{ .jw = jw, .tree = self };
|
||||||
@@ -45,7 +47,7 @@ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) error{WriteFailed}!
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| {
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
log.err(.app, "semantic tree json dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -58,7 +60,7 @@ pub fn textStringify(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!v
|
|||||||
log.err(.app, "listener map failed", .{ .err = err });
|
log.err(.app, "listener map failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets) catch |err| {
|
self.walk(self.dom_node, &xpath_buffer, null, &visitor, 1, listener_targets, 0) catch |err| {
|
||||||
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
log.err(.app, "semantic tree text dump failed", .{ .err = err });
|
||||||
return error.WriteFailed;
|
return error.WriteFailed;
|
||||||
};
|
};
|
||||||
@@ -71,7 +73,7 @@ const OptionData = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NodeData = struct {
|
const NodeData = struct {
|
||||||
id: u32,
|
id: CDPNode.Id,
|
||||||
axn: AXNode,
|
axn: AXNode,
|
||||||
role: []const u8,
|
role: []const u8,
|
||||||
name: ?[]const u8,
|
name: ?[]const u8,
|
||||||
@@ -82,7 +84,9 @@ const NodeData = struct {
|
|||||||
node_name: []const u8,
|
node_name: []const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap) !void {
|
fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_name: ?[]const u8, visitor: anytype, index: usize, listener_targets: interactive.ListenerTargetMap, current_depth: u32) !void {
|
||||||
|
if (current_depth > self.max_depth) return;
|
||||||
|
|
||||||
// 1. Skip non-content nodes
|
// 1. Skip non-content nodes
|
||||||
if (node.is(Element)) |el| {
|
if (node.is(Element)) |el| {
|
||||||
const tag = el.getTag();
|
const tag = el.getTag();
|
||||||
@@ -174,7 +178,23 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
};
|
};
|
||||||
|
|
||||||
var should_visit = true;
|
var should_visit = true;
|
||||||
if (self.prune) {
|
if (self.interactive_only) {
|
||||||
|
var keep = false;
|
||||||
|
if (interactive.isInteractiveRole(role)) {
|
||||||
|
keep = true;
|
||||||
|
} else if (interactive.isContentRole(role)) {
|
||||||
|
if (name != null and name.?.len > 0) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
} else if (std.mem.eql(u8, role, "RootWebArea")) {
|
||||||
|
keep = true;
|
||||||
|
} else if (is_interactive) {
|
||||||
|
keep = true;
|
||||||
|
}
|
||||||
|
if (!keep) {
|
||||||
|
should_visit = false;
|
||||||
|
}
|
||||||
|
} else if (self.prune) {
|
||||||
if (structural and !is_interactive and !has_explicit_label) {
|
if (structural and !is_interactive and !has_explicit_label) {
|
||||||
should_visit = false;
|
should_visit = false;
|
||||||
}
|
}
|
||||||
@@ -213,7 +233,7 @@ fn walk(self: @This(), node: *Node, xpath_buffer: *std.ArrayList(u8), parent_nam
|
|||||||
}
|
}
|
||||||
gop.value_ptr.* += 1;
|
gop.value_ptr.* += 1;
|
||||||
|
|
||||||
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets);
|
try self.walk(child, xpath_buffer, name, visitor, gop.value_ptr.*, listener_targets, current_depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,36 +409,45 @@ const TextVisitor = struct {
|
|||||||
depth: usize,
|
depth: usize,
|
||||||
|
|
||||||
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
pub fn visit(self: *TextVisitor, node: *Node, data: *NodeData) !bool {
|
||||||
// Format: " [12] link: Hacker News (value)"
|
for (0..self.depth) |_| {
|
||||||
for (0..(self.depth * 2)) |_| {
|
|
||||||
try self.writer.writeByte(' ');
|
try self.writer.writeByte(' ');
|
||||||
}
|
}
|
||||||
try self.writer.print("[{d}] {s}: ", .{ data.id, data.role });
|
|
||||||
|
|
||||||
|
var name_to_print: ?[]const u8 = null;
|
||||||
if (data.name) |n| {
|
if (data.name) |n| {
|
||||||
if (n.len > 0) {
|
if (n.len > 0) {
|
||||||
try self.writer.writeAll(n);
|
name_to_print = n;
|
||||||
}
|
}
|
||||||
} else if (node.is(CData.Text)) |text_node| {
|
} else if (node.is(CData.Text)) |text_node| {
|
||||||
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n");
|
||||||
if (trimmed.len > 0) {
|
if (trimmed.len > 0) {
|
||||||
try self.writer.writeAll(trimmed);
|
name_to_print = trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const is_text_only = std.mem.eql(u8, data.role, "StaticText") or std.mem.eql(u8, data.role, "none") or std.mem.eql(u8, data.role, "generic");
|
||||||
|
|
||||||
|
try self.writer.print("{d}", .{data.id});
|
||||||
|
if (!is_text_only) {
|
||||||
|
try self.writer.print(" {s}", .{data.role});
|
||||||
|
}
|
||||||
|
if (name_to_print) |n| {
|
||||||
|
try self.writer.print(" '{s}'", .{n});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.value) |v| {
|
if (data.value) |v| {
|
||||||
if (v.len > 0) {
|
if (v.len > 0) {
|
||||||
try self.writer.print(" (value: {s})", .{v});
|
try self.writer.print(" value='{s}'", .{v});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.options) |options| {
|
if (data.options) |options| {
|
||||||
try self.writer.writeAll(" options: [");
|
try self.writer.writeAll(" options=[");
|
||||||
for (options, 0..) |opt, i| {
|
for (options, 0..) |opt, i| {
|
||||||
if (i > 0) try self.writer.writeAll(", ");
|
if (i > 0) try self.writer.writeAll(",");
|
||||||
try self.writer.print("'{s}'", .{opt.value});
|
try self.writer.print("'{s}'", .{opt.value});
|
||||||
if (opt.selected) {
|
if (opt.selected) {
|
||||||
try self.writer.writeAll(" (selected)");
|
try self.writer.writeAll("*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try self.writer.writeAll("]\n");
|
try self.writer.writeAll("]\n");
|
||||||
@@ -448,3 +477,56 @@ const TextVisitor = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("testing.zig");
|
||||||
|
|
||||||
|
test "SemanticTree backendDOMNodeId" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = std.math.maxInt(u32) - 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const json_str = try std.json.Stringify.valueAlloc(testing.allocator, st, .{});
|
||||||
|
defer testing.allocator.free(json_str);
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, json_str, "\"backendDOMNodeId\":") != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "SemanticTree max_depth" {
|
||||||
|
var registry: CDPNode.Registry = .init(testing.allocator);
|
||||||
|
defer registry.deinit();
|
||||||
|
|
||||||
|
var page = try testing.pageTest("cdp/registry1.html");
|
||||||
|
defer testing.reset();
|
||||||
|
defer page._session.removePage();
|
||||||
|
|
||||||
|
const st: Self = .{
|
||||||
|
.dom_node = page.window._document.asNode(),
|
||||||
|
.registry = ®istry,
|
||||||
|
.page = page,
|
||||||
|
.arena = testing.arena_allocator,
|
||||||
|
.prune = false,
|
||||||
|
.interactive_only = false,
|
||||||
|
.max_depth = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||||
|
defer aw.deinit();
|
||||||
|
|
||||||
|
try st.textStringify(&aw.writer);
|
||||||
|
const text_str = aw.written();
|
||||||
|
|
||||||
|
try testing.expect(std.mem.indexOf(u8, text_str, "other") == null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,17 +64,17 @@ pub fn init(app: *App, address: net.Address) !*Server {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Server) void {
|
pub fn shutdown(self: *Server) void {
|
||||||
// Stop all active clients
|
self.client_mutex.lock();
|
||||||
{
|
defer self.client_mutex.unlock();
|
||||||
self.client_mutex.lock();
|
|
||||||
defer self.client_mutex.unlock();
|
|
||||||
|
|
||||||
for (self.clients.items) |client| {
|
for (self.clients.items) |client| {
|
||||||
client.stop();
|
client.stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Server) void {
|
||||||
|
self.shutdown();
|
||||||
self.joinThreads();
|
self.joinThreads();
|
||||||
self.clients.deinit(self.allocator);
|
self.clients.deinit(self.allocator);
|
||||||
self.clients_pool.deinit();
|
self.clients_pool.deinit();
|
||||||
@@ -242,7 +242,10 @@ pub const Client = struct {
|
|||||||
fn stop(self: *Client) void {
|
fn stop(self: *Client) void {
|
||||||
switch (self.mode) {
|
switch (self.mode) {
|
||||||
.http => {},
|
.http => {},
|
||||||
.cdp => |*cdp| cdp.browser.env.terminate(),
|
.cdp => |*cdp| {
|
||||||
|
cdp.browser.env.terminate();
|
||||||
|
self.ws.sendClose();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
self.ws.shutdown();
|
self.ws.shutdown();
|
||||||
}
|
}
|
||||||
@@ -295,7 +298,7 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cdp = &self.mode.cdp;
|
var cdp = &self.mode.cdp;
|
||||||
var last_message = timestamp(.monotonic);
|
var last_message = milliTimestamp(.monotonic);
|
||||||
var ms_remaining = self.ws.timeout_ms;
|
var ms_remaining = self.ws.timeout_ms;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -304,7 +307,7 @@ pub const Client = struct {
|
|||||||
if (self.readSocket() == false) {
|
if (self.readSocket() == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_message = timestamp(.monotonic);
|
last_message = milliTimestamp(.monotonic);
|
||||||
ms_remaining = self.ws.timeout_ms;
|
ms_remaining = self.ws.timeout_ms;
|
||||||
},
|
},
|
||||||
.no_page => {
|
.no_page => {
|
||||||
@@ -319,16 +322,18 @@ pub const Client = struct {
|
|||||||
if (self.readSocket() == false) {
|
if (self.readSocket() == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
last_message = timestamp(.monotonic);
|
last_message = milliTimestamp(.monotonic);
|
||||||
ms_remaining = self.ws.timeout_ms;
|
ms_remaining = self.ws.timeout_ms;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => {
|
||||||
const elapsed = timestamp(.monotonic) - last_message;
|
const now = milliTimestamp(.monotonic);
|
||||||
if (elapsed > ms_remaining) {
|
const elapsed = now - last_message;
|
||||||
|
if (elapsed >= ms_remaining) {
|
||||||
log.info(.app, "CDP timeout", .{});
|
log.info(.app, "CDP timeout", .{});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ms_remaining -= @intCast(elapsed);
|
ms_remaining -= @intCast(elapsed);
|
||||||
|
last_message = now;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,6 +506,7 @@ fn buildJSONVersionResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const timestamp = @import("datetime.zig").timestamp;
|
pub const timestamp = @import("datetime.zig").timestamp;
|
||||||
|
pub const milliTimestamp = @import("datetime.zig").milliTimestamp;
|
||||||
|
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
test "server: buildJSONVersionResponse" {
|
test "server: buildJSONVersionResponse" {
|
||||||
|
|||||||
@@ -91,25 +91,32 @@ pub fn runMicrotasks(self: *Browser) void {
|
|||||||
self.env.runMicrotasks();
|
self.env.runMicrotasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Browser) !?u64 {
|
pub fn runMacrotasks(self: *Browser) !void {
|
||||||
const env = &self.env;
|
const env = &self.env;
|
||||||
|
|
||||||
const time_to_next = try self.env.runMacrotasks();
|
try self.env.runMacrotasks();
|
||||||
env.pumpMessageLoop();
|
env.pumpMessageLoop();
|
||||||
|
|
||||||
// either of the above could have queued more microtasks
|
// either of the above could have queued more microtasks
|
||||||
env.runMicrotasks();
|
env.runMicrotasks();
|
||||||
|
|
||||||
return time_to_next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasBackgroundTasks(self: *Browser) bool {
|
pub fn hasBackgroundTasks(self: *Browser) bool {
|
||||||
return self.env.hasBackgroundTasks();
|
return self.env.hasBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waitForBackgroundTasks(self: *Browser) void {
|
pub fn waitForBackgroundTasks(self: *Browser) void {
|
||||||
self.env.waitForBackgroundTasks();
|
self.env.waitForBackgroundTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn msToNextMacrotask(self: *Browser) ?u64 {
|
||||||
|
return self.env.msToNextMacrotask();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msTo(self: *Browser) bool {
|
||||||
|
return self.env.hasBackgroundTasks();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runIdleTasks(self: *const Browser) void {
|
pub fn runIdleTasks(self: *const Browser) void {
|
||||||
self.env.runIdleTasks();
|
self.env.runIdleTasks();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,6 +233,12 @@ const DispatchDirectOptions = struct {
|
|||||||
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
event.acquireRef();
|
event.acquireRef();
|
||||||
defer event.deinit(false, page._session);
|
defer event.deinit(false, page._session);
|
||||||
|
|
||||||
@@ -398,6 +404,13 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
// Set window.event to the currently dispatching event (WHATWG spec)
|
||||||
|
const window = page.window;
|
||||||
|
const prev_event = window._current_event;
|
||||||
|
window._current_event = event;
|
||||||
|
defer window._current_event = prev_event;
|
||||||
|
|
||||||
var was_handled = false;
|
var was_handled = false;
|
||||||
|
|
||||||
// Create a single scope for all event handlers in this dispatch.
|
// Create a single scope for all event handlers in this dispatch.
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ use_proxy: bool,
|
|||||||
// Current TLS verification state, applied per-connection in makeRequest.
|
// Current TLS verification state, applied per-connection in makeRequest.
|
||||||
tls_verify: bool = true,
|
tls_verify: bool = true,
|
||||||
|
|
||||||
|
obey_robots: bool,
|
||||||
|
|
||||||
cdp_client: ?CDPClient = null,
|
cdp_client: ?CDPClient = null,
|
||||||
|
|
||||||
// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll
|
// libcurl can monitor arbitrary sockets, this lets us use libcurl to poll
|
||||||
@@ -154,6 +156,7 @@ pub fn init(allocator: Allocator, network: *Network) !*Client {
|
|||||||
.http_proxy = http_proxy,
|
.http_proxy = http_proxy,
|
||||||
.use_proxy = http_proxy != null,
|
.use_proxy = http_proxy != null,
|
||||||
.tls_verify = network.config.tlsVerifyHost(),
|
.tls_verify = network.config.tlsVerifyHost(),
|
||||||
|
.obey_robots = network.config.obeyRobots(),
|
||||||
.transfer_pool = transfer_pool,
|
.transfer_pool = transfer_pool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -257,34 +260,33 @@ pub fn tick(self: *Client, timeout_ms: u32) !PerformStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(self: *Client, req: Request) !void {
|
pub fn request(self: *Client, req: Request) !void {
|
||||||
if (self.network.config.obeyRobots()) {
|
if (self.obey_robots == false) {
|
||||||
const robots_url = try URL.getRobotsUrl(self.allocator, req.url);
|
return self.processRequest(req);
|
||||||
errdefer self.allocator.free(robots_url);
|
|
||||||
|
|
||||||
// If we have this robots cached, we can take a fast path.
|
|
||||||
if (self.network.robot_store.get(robots_url)) |robot_entry| {
|
|
||||||
defer self.allocator.free(robots_url);
|
|
||||||
|
|
||||||
switch (robot_entry) {
|
|
||||||
// If we have a found robots entry, we check it.
|
|
||||||
.present => |robots| {
|
|
||||||
const path = URL.getPathname(req.url);
|
|
||||||
if (!robots.isAllowed(path)) {
|
|
||||||
req.error_callback(req.ctx, error.RobotsBlocked);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Otherwise, we assume we won't find it again.
|
|
||||||
.absent => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.processRequest(req);
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.processRequest(req);
|
const robots_url = try URL.getRobotsUrl(self.allocator, req.url);
|
||||||
|
errdefer self.allocator.free(robots_url);
|
||||||
|
|
||||||
|
// If we have this robots cached, we can take a fast path.
|
||||||
|
if (self.network.robot_store.get(robots_url)) |robot_entry| {
|
||||||
|
defer self.allocator.free(robots_url);
|
||||||
|
|
||||||
|
switch (robot_entry) {
|
||||||
|
// If we have a found robots entry, we check it.
|
||||||
|
.present => |robots| {
|
||||||
|
const path = URL.getPathname(req.url);
|
||||||
|
if (!robots.isAllowed(path)) {
|
||||||
|
req.error_callback(req.ctx, error.RobotsBlocked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Otherwise, we assume we won't find it again.
|
||||||
|
.absent => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.processRequest(req);
|
||||||
|
}
|
||||||
|
return self.fetchRobotsThenProcessRequest(robots_url, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn processRequest(self: *Client, req: Request) !void {
|
fn processRequest(self: *Client, req: Request) !void {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ params: []const u8 = "",
|
|||||||
// We keep 41 for null-termination since HTML parser expects in this format.
|
// We keep 41 for null-termination since HTML parser expects in this format.
|
||||||
charset: [41]u8 = default_charset,
|
charset: [41]u8 = default_charset,
|
||||||
charset_len: usize = default_charset_len,
|
charset_len: usize = default_charset_len,
|
||||||
|
is_default_charset: bool = true,
|
||||||
|
|
||||||
/// String "UTF-8" continued by null characters.
|
/// String "UTF-8" continued by null characters.
|
||||||
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36;
|
||||||
@@ -130,6 +131,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
|
|
||||||
var charset: [41]u8 = default_charset;
|
var charset: [41]u8 = default_charset;
|
||||||
var charset_len: usize = default_charset_len;
|
var charset_len: usize = default_charset_len;
|
||||||
|
var has_explicit_charset = false;
|
||||||
|
|
||||||
var it = std.mem.splitScalar(u8, params, ';');
|
var it = std.mem.splitScalar(u8, params, ';');
|
||||||
while (it.next()) |attr| {
|
while (it.next()) |attr| {
|
||||||
@@ -156,6 +158,7 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
// Null-terminate right after attribute value.
|
// Null-terminate right after attribute value.
|
||||||
charset[attribute_value.len] = 0;
|
charset[attribute_value.len] = 0;
|
||||||
charset_len = attribute_value.len;
|
charset_len = attribute_value.len;
|
||||||
|
has_explicit_charset = true;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,9 +168,137 @@ pub fn parse(input: []u8) !Mime {
|
|||||||
.charset = charset,
|
.charset = charset,
|
||||||
.charset_len = charset_len,
|
.charset_len = charset_len,
|
||||||
.content_type = content_type,
|
.content_type = content_type,
|
||||||
|
.is_default_charset = !has_explicit_charset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prescan the first 1024 bytes of an HTML document for a charset declaration.
|
||||||
|
/// Looks for `<meta charset="X">` and `<meta http-equiv="Content-Type" content="...;charset=X">`.
|
||||||
|
/// Returns the charset value or null if none found.
|
||||||
|
/// See: https://www.w3.org/International/questions/qa-html-encoding-declarations
|
||||||
|
pub fn prescanCharset(html: []const u8) ?[]const u8 {
|
||||||
|
const limit = @min(html.len, 1024);
|
||||||
|
const data = html[0..limit];
|
||||||
|
|
||||||
|
// Scan for <meta tags
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < data.len) {
|
||||||
|
// Find next '<'
|
||||||
|
pos = std.mem.indexOfScalarPos(u8, data, pos, '<') orelse return null;
|
||||||
|
pos += 1;
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
|
||||||
|
// Check for "meta" (case-insensitive)
|
||||||
|
if (pos + 4 >= data.len) return null;
|
||||||
|
var tag_buf: [4]u8 = undefined;
|
||||||
|
_ = std.ascii.lowerString(&tag_buf, data[pos..][0..4]);
|
||||||
|
if (!std.mem.eql(u8, &tag_buf, "meta")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
// Must be followed by whitespace or end of tag
|
||||||
|
if (pos >= data.len) return null;
|
||||||
|
if (data[pos] != ' ' and data[pos] != '\t' and data[pos] != '\n' and
|
||||||
|
data[pos] != '\r' and data[pos] != '/')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan attributes within this meta tag
|
||||||
|
const tag_end = std.mem.indexOfScalarPos(u8, data, pos, '>') orelse return null;
|
||||||
|
const attrs = data[pos..tag_end];
|
||||||
|
|
||||||
|
// Look for charset= attribute directly
|
||||||
|
if (findAttrValue(attrs, "charset")) |charset| {
|
||||||
|
if (charset.len > 0 and charset.len <= 40) return charset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for http-equiv="content-type" with content="...;charset=X"
|
||||||
|
if (findAttrValue(attrs, "http-equiv")) |he| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(he, "content-type")) {
|
||||||
|
if (findAttrValue(attrs, "content")) |content| {
|
||||||
|
if (extractCharsetFromContentType(content)) |charset| {
|
||||||
|
return charset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = tag_end + 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findAttrValue(attrs: []const u8, name: []const u8) ?[]const u8 {
|
||||||
|
var pos: usize = 0;
|
||||||
|
while (pos < attrs.len) {
|
||||||
|
// Skip whitespace
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t' or
|
||||||
|
attrs[pos] == '\n' or attrs[pos] == '\r'))
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute name
|
||||||
|
const attr_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != '=' and attrs[pos] != ' ' and
|
||||||
|
attrs[pos] != '\t' and attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
const attr_name = attrs[attr_start..pos];
|
||||||
|
|
||||||
|
// Skip whitespace around =
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len or attrs[pos] != '=') {
|
||||||
|
// No '=' found - skip this token. Advance at least one byte to avoid infinite loop.
|
||||||
|
if (pos == attr_start) pos += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pos += 1; // skip '='
|
||||||
|
while (pos < attrs.len and (attrs[pos] == ' ' or attrs[pos] == '\t')) pos += 1;
|
||||||
|
if (pos >= attrs.len) return null;
|
||||||
|
|
||||||
|
// Read attribute value
|
||||||
|
const value = blk: {
|
||||||
|
if (attrs[pos] == '"' or attrs[pos] == '\'') {
|
||||||
|
const quote = attrs[pos];
|
||||||
|
pos += 1;
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != quote) pos += 1;
|
||||||
|
const val = attrs[val_start..pos];
|
||||||
|
if (pos < attrs.len) pos += 1; // skip closing quote
|
||||||
|
break :blk val;
|
||||||
|
} else {
|
||||||
|
const val_start = pos;
|
||||||
|
while (pos < attrs.len and attrs[pos] != ' ' and attrs[pos] != '\t' and
|
||||||
|
attrs[pos] != '>' and attrs[pos] != '/')
|
||||||
|
{
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
break :blk attrs[val_start..pos];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(attr_name, name)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extractCharsetFromContentType(content: []const u8) ?[]const u8 {
|
||||||
|
var it = std.mem.splitScalar(u8, content, ';');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
const trimmed = std.mem.trimLeft(u8, part, &.{ ' ', '\t' });
|
||||||
|
if (trimmed.len > 8 and std.ascii.eqlIgnoreCase(trimmed[0..8], "charset=")) {
|
||||||
|
const val = std.mem.trim(u8, trimmed[8..], &.{ ' ', '\t', '"', '\'' });
|
||||||
|
if (val.len > 0 and val.len <= 40) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn sniff(body: []const u8) ?Mime {
|
pub fn sniff(body: []const u8) ?Mime {
|
||||||
// 0x0C is form feed
|
// 0x0C is form feed
|
||||||
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
const content = std.mem.trimLeft(u8, body, &.{ ' ', '\t', '\n', '\r', 0x0C });
|
||||||
@@ -178,15 +309,30 @@ pub fn sniff(body: []const u8) ?Mime {
|
|||||||
if (content[0] != '<') {
|
if (content[0] != '<') {
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xEF, 0xBB, 0xBF })) {
|
||||||
// UTF-8 BOM
|
// UTF-8 BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = default_charset,
|
||||||
|
.charset_len = default_charset_len,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFE, 0xFF })) {
|
||||||
// UTF-16 big-endian BOM
|
// UTF-16 big-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'B', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
if (std.mem.startsWith(u8, content, &.{ 0xFF, 0xFE })) {
|
||||||
// UTF-16 little-endian BOM
|
// UTF-16 little-endian BOM
|
||||||
return .{ .content_type = .{ .text_plain = {} } };
|
return .{
|
||||||
|
.content_type = .{ .text_plain = {} },
|
||||||
|
.charset = .{ 'U', 'T', 'F', '-', '1', '6', 'L', 'E' } ++ .{0} ** 33,
|
||||||
|
.charset_len = 8,
|
||||||
|
.is_default_charset = false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -540,6 +686,24 @@ test "Mime: sniff" {
|
|||||||
|
|
||||||
try expectHTML("<!-->");
|
try expectHTML("<!-->");
|
||||||
try expectHTML(" \n\t <!-->");
|
try expectHTML(" \n\t <!-->");
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xEF, 0xBB, 0xBF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-8", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFE, 0xFF }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16BE", mime.charsetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const mime = Mime.sniff(&.{ 0xFF, 0xFE }).?;
|
||||||
|
try testing.expectEqual(.text_plain, std.meta.activeTag(mime.content_type));
|
||||||
|
try testing.expectEqual("UTF-16LE", mime.charsetString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Expectation = struct {
|
const Expectation = struct {
|
||||||
@@ -576,3 +740,35 @@ fn expect(expected: Expectation, input: []const u8) !void {
|
|||||||
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
try testing.expectEqual(m.charsetStringZ(), actual.charsetStringZ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Mime: prescanCharset" {
|
||||||
|
// <meta charset="X">
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<html><head><meta charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("iso-8859-1", Mime.prescanCharset("<html><head><meta charset=\"iso-8859-1\">").?);
|
||||||
|
try testing.expectEqual("shift_jis", Mime.prescanCharset("<meta charset='shift_jis'>").?);
|
||||||
|
|
||||||
|
// Case-insensitive tag matching
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<META charset=\"utf-8\">").?);
|
||||||
|
try testing.expectEqual("utf-8", Mime.prescanCharset("<Meta charset=\"utf-8\">").?);
|
||||||
|
|
||||||
|
// <meta http-equiv="Content-Type" content="text/html; charset=X">
|
||||||
|
try testing.expectEqual(
|
||||||
|
"iso-8859-1",
|
||||||
|
Mime.prescanCharset("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">").?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// No charset found
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<html><head><title>Test</title>"));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(""));
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("no html here"));
|
||||||
|
|
||||||
|
// Self-closing meta without charset must not loop forever
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset("<meta foo=\"bar\"/>"));
|
||||||
|
|
||||||
|
// Charset after 1024 bytes should not be found
|
||||||
|
var long_html: [1100]u8 = undefined;
|
||||||
|
@memset(&long_html, ' ');
|
||||||
|
const suffix = "<meta charset=\"windows-1252\">";
|
||||||
|
@memcpy(long_html[1050 .. 1050 + suffix.len], suffix);
|
||||||
|
try testing.expectEqual(null, Mime.prescanCharset(&long_html));
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const storage = @import("webapi/storage/storage.zig");
|
|||||||
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
|
||||||
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
|
||||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
|
||||||
const HttpClient = @import("HttpClient.zig");
|
const HttpClient = @import("HttpClient.zig");
|
||||||
const ArenaPool = App.ArenaPool;
|
const ArenaPool = App.ArenaPool;
|
||||||
@@ -307,14 +308,16 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
|
|||||||
document._page = self;
|
document._page = self;
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// HTML test runner manually calls these as necessary
|
if (parent == null) {
|
||||||
try self.js.scheduler.add(session.browser, struct {
|
// HTML test runner manually calls these as necessary
|
||||||
fn runIdleTasks(ctx: *anyopaque) !?u32 {
|
try self.js.scheduler.add(session.browser, struct {
|
||||||
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
fn runIdleTasks(ctx: *anyopaque) !?u32 {
|
||||||
b.runIdleTasks();
|
const b: *@import("Browser.zig") = @ptrCast(@alignCast(ctx));
|
||||||
return 200;
|
b.runIdleTasks();
|
||||||
}
|
return 200;
|
||||||
}.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true });
|
}
|
||||||
|
}.runIdleTasks, 200, .{ .name = "page.runIdleTasks", .low_priority = true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,16 +410,9 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
|||||||
return std.mem.startsWith(u8, url, current_origin);
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up a blob URL in this page's registry, walking up the parent chain.
|
/// Look up a blob URL in this page's registry.
|
||||||
pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {
|
pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {
|
||||||
var current: ?*Page = self;
|
return self._blob_urls.get(url);
|
||||||
while (current) |page| {
|
|
||||||
if (page._blob_urls.get(url)) |blob| {
|
|
||||||
return blob;
|
|
||||||
}
|
|
||||||
current = page.parent;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
||||||
@@ -457,7 +453,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
|
|
||||||
// Content injection
|
// Content injection
|
||||||
if (is_blob) {
|
if (is_blob) {
|
||||||
const blob = self.lookupBlobUrl(request_url) orelse {
|
// For navigation, walk up the parent chain to find blob URLs
|
||||||
|
// (e.g., parent creates blob URL and sets iframe.src to it)
|
||||||
|
const blob = blk: {
|
||||||
|
var current: ?*Page = self.parent;
|
||||||
|
while (current) |page| {
|
||||||
|
if (page._blob_urls.get(request_url)) |b| break :blk b;
|
||||||
|
current = page.parent;
|
||||||
|
}
|
||||||
log.warn(.js, "invalid blob", .{ .url = request_url });
|
log.warn(.js, "invalid blob", .{ .url = request_url });
|
||||||
return error.BlobNotFound;
|
return error.BlobNotFound;
|
||||||
};
|
};
|
||||||
@@ -709,11 +712,14 @@ pub fn scriptsCompletedLoading(self: *Page) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
||||||
blk: {
|
var ls: JS.Local.Scope = undefined;
|
||||||
var ls: JS.Local.Scope = undefined;
|
self.js.localScope(&ls);
|
||||||
self.js.localScope(&ls);
|
defer ls.deinit();
|
||||||
defer ls.deinit();
|
|
||||||
|
|
||||||
|
const entered = self.js.enter(&ls.handle_scope);
|
||||||
|
defer entered.exit();
|
||||||
|
|
||||||
|
blk: {
|
||||||
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
const event = Event.initTrusted(comptime .wrap("load"), .{}, self) catch |err| {
|
||||||
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
|
log.err(.page, "iframe event init", .{ .err = err, .url = iframe._src });
|
||||||
break :blk;
|
break :blk;
|
||||||
@@ -722,6 +728,7 @@ pub fn iframeCompletedLoading(self: *Page, iframe: *IFrame) void {
|
|||||||
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
log.warn(.js, "iframe onload", .{ .err = err, .url = iframe._src });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pendingLoadCompleted();
|
self.pendingLoadCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,13 +855,25 @@ fn pageDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
|||||||
if (self._parse_state == .pre) {
|
if (self._parse_state == .pre) {
|
||||||
// we lazily do this, because we might need the first chunk of data
|
// we lazily do this, because we might need the first chunk of data
|
||||||
// to sniff the content type
|
// to sniff the content type
|
||||||
const mime: Mime = blk: {
|
var mime: Mime = blk: {
|
||||||
if (transfer.response_header.?.contentType()) |ct| {
|
if (transfer.response_header.?.contentType()) |ct| {
|
||||||
break :blk try Mime.parse(ct);
|
break :blk try Mime.parse(ct);
|
||||||
}
|
}
|
||||||
break :blk Mime.sniff(data);
|
break :blk Mime.sniff(data);
|
||||||
} orelse .unknown;
|
} orelse .unknown;
|
||||||
|
|
||||||
|
// If the HTTP Content-Type header didn't specify a charset and this is HTML,
|
||||||
|
// prescan the first 1024 bytes for a <meta charset> declaration.
|
||||||
|
if (mime.content_type == .text_html and mime.is_default_charset) {
|
||||||
|
if (Mime.prescanCharset(data)) |charset| {
|
||||||
|
if (charset.len <= 40) {
|
||||||
|
@memcpy(mime.charset[0..charset.len], charset);
|
||||||
|
mime.charset[charset.len] = 0;
|
||||||
|
mime.charset_len = charset.len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.page, "navigate first chunk", .{
|
log.debug(.page, "navigate first chunk", .{
|
||||||
.content_type = mime.content_type,
|
.content_type = mime.content_type,
|
||||||
@@ -1091,7 +1110,6 @@ pub fn iframeAddedCallback(self: *Page, iframe: *IFrame) !void {
|
|||||||
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err });
|
||||||
self._pending_loads -= 1;
|
self._pending_loads -= 1;
|
||||||
iframe._window = null;
|
iframe._window = null;
|
||||||
page_frame.deinit(true);
|
|
||||||
return error.IFrameLoadError;
|
return error.IFrameLoadError;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3256,14 +3274,14 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
|||||||
.type = self._type,
|
.type = self._type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const event = (try @import("webapi/event/MouseEvent.zig").init("click", .{
|
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||||
.bubbles = true,
|
.bubbles = true,
|
||||||
.cancelable = true,
|
.cancelable = true,
|
||||||
.composed = true,
|
.composed = true,
|
||||||
.clientX = x,
|
.clientX = x,
|
||||||
.clientY = y,
|
.clientY = y,
|
||||||
}, self)).asEvent();
|
}, self);
|
||||||
try self._event_manager.dispatch(target.asEventTarget(), event);
|
try self._event_manager.dispatch(target.asEventTarget(), mouse_event.asEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback when the "click" event reaches the pages.
|
// callback when the "click" event reaches the pages.
|
||||||
@@ -3507,13 +3525,16 @@ fn asUint(comptime string: anytype) std.meta.Int(
|
|||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "WebApi: Page" {
|
test "WebApi: Page" {
|
||||||
const filter: testing.LogFilter = .init(.http);
|
const filter: testing.LogFilter = .init(&.{ .http, .js });
|
||||||
defer filter.deinit();
|
defer filter.deinit();
|
||||||
|
|
||||||
try testing.htmlRunner("page", .{});
|
try testing.htmlRunner("page", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
test "WebApi: Frames" {
|
test "WebApi: Frames" {
|
||||||
|
const filter: testing.LogFilter = .init(&.{.js});
|
||||||
|
defer filter.deinit();
|
||||||
|
|
||||||
try testing.htmlRunner("frames", .{});
|
try testing.htmlRunner("frames", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ shutdown: bool = false,
|
|||||||
|
|
||||||
client: *HttpClient,
|
client: *HttpClient,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
|
||||||
|
|
||||||
script_pool: std.heap.MemoryPool(Script),
|
|
||||||
|
|
||||||
// We can download multiple sync modules in parallel, but we want to process
|
// We can download multiple sync modules in parallel, but we want to process
|
||||||
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
// them in order. We can't use an std.DoublyLinkedList, like the other script types,
|
||||||
@@ -101,18 +98,14 @@ pub fn init(allocator: Allocator, http_client: *HttpClient, page: *Page) ScriptM
|
|||||||
.imported_modules = .empty,
|
.imported_modules = .empty,
|
||||||
.client = http_client,
|
.client = http_client,
|
||||||
.static_scripts_done = false,
|
.static_scripts_done = false,
|
||||||
.buffer_pool = BufferPool.init(allocator, 5),
|
|
||||||
.page_notified_of_completion = false,
|
.page_notified_of_completion = false,
|
||||||
.script_pool = std.heap.MemoryPool(Script).init(allocator),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *ScriptManager) void {
|
pub fn deinit(self: *ScriptManager) void {
|
||||||
// necessary to free any buffers scripts may be referencing
|
// necessary to free any arenas scripts may be referencing
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
self.buffer_pool.deinit();
|
|
||||||
self.script_pool.deinit();
|
|
||||||
self.imported_modules.deinit(self.allocator);
|
self.imported_modules.deinit(self.allocator);
|
||||||
// we don't deinit self.importmap b/c we use the page's arena for its
|
// we don't deinit self.importmap b/c we use the page's arena for its
|
||||||
// allocations.
|
// allocations.
|
||||||
@@ -121,7 +114,10 @@ pub fn deinit(self: *ScriptManager) void {
|
|||||||
pub fn reset(self: *ScriptManager) void {
|
pub fn reset(self: *ScriptManager) void {
|
||||||
var it = self.imported_modules.valueIterator();
|
var it = self.imported_modules.valueIterator();
|
||||||
while (it.next()) |value_ptr| {
|
while (it.next()) |value_ptr| {
|
||||||
self.buffer_pool.release(value_ptr.buffer);
|
switch (value_ptr.state) {
|
||||||
|
.done => |script| script.deinit(),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.imported_modules.clearRetainingCapacity();
|
self.imported_modules.clearRetainingCapacity();
|
||||||
|
|
||||||
@@ -138,13 +134,13 @@ pub fn reset(self: *ScriptManager) void {
|
|||||||
fn clearList(list: *std.DoublyLinkedList) void {
|
fn clearList(list: *std.DoublyLinkedList) void {
|
||||||
while (list.popFirst()) |n| {
|
while (list.popFirst()) |n| {
|
||||||
const script: *Script = @fieldParentPtr("node", n);
|
const script: *Script = @fieldParentPtr("node", n);
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getHeaders(self: *ScriptManager, url: [:0]const u8) !net_http.Headers {
|
fn getHeaders(self: *ScriptManager, arena: Allocator, url: [:0]const u8) !net_http.Headers {
|
||||||
var headers = try self.client.newHeaders();
|
var headers = try self.client.newHeaders();
|
||||||
try self.page.headersForRequest(self.page.arena, url, &headers);
|
try self.page.headersForRequest(arena, url, &headers);
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,19 +187,26 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handover = false;
|
||||||
const page = self.page;
|
const page = self.page;
|
||||||
|
|
||||||
|
const arena = try page.getArena(.{ .debug = "addFromElement" });
|
||||||
|
errdefer if (!handover) {
|
||||||
|
page.releaseArena(arena);
|
||||||
|
};
|
||||||
|
|
||||||
var source: Script.Source = undefined;
|
var source: Script.Source = undefined;
|
||||||
var remote_url: ?[:0]const u8 = null;
|
var remote_url: ?[:0]const u8 = null;
|
||||||
const base_url = page.base();
|
const base_url = page.base();
|
||||||
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
if (element.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
if (try parseDataURI(page.arena, src)) |data_uri| {
|
if (try parseDataURI(arena, src)) |data_uri| {
|
||||||
source = .{ .@"inline" = data_uri };
|
source = .{ .@"inline" = data_uri };
|
||||||
} else {
|
} else {
|
||||||
remote_url = try URL.resolve(page.arena, base_url, src, .{});
|
remote_url = try URL.resolve(arena, base_url, src, .{});
|
||||||
source = .{ .remote = .{} };
|
source = .{ .remote = .{} };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var buf = std.Io.Writer.Allocating.init(page.arena);
|
var buf = std.Io.Writer.Allocating.init(arena);
|
||||||
try element.asNode().getChildTextContent(&buf.writer);
|
try element.asNode().getChildTextContent(&buf.writer);
|
||||||
try buf.writer.writeByte(0);
|
try buf.writer.writeByte(0);
|
||||||
const data = buf.written();
|
const data = buf.written();
|
||||||
@@ -211,6 +214,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
if (inline_source.len == 0) {
|
if (inline_source.len == 0) {
|
||||||
// we haven't set script_element._executed = true yet, which is good.
|
// we haven't set script_element._executed = true yet, which is good.
|
||||||
// If content is appended to the script, we will execute it then.
|
// If content is appended to the script, we will execute it then.
|
||||||
|
page.releaseArena(arena);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
source = .{ .@"inline" = inline_source };
|
source = .{ .@"inline" = inline_source };
|
||||||
@@ -218,15 +222,13 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
|
|
||||||
// Only set _executed (already-started) when we actually have content to execute
|
// Only set _executed (already-started) when we actually have content to execute
|
||||||
script_element._executed = true;
|
script_element._executed = true;
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
|
||||||
errdefer self.script_pool.destroy(script);
|
|
||||||
|
|
||||||
const is_inline = source == .@"inline";
|
const is_inline = source == .@"inline";
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = kind,
|
.kind = kind,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
|
.arena = arena,
|
||||||
.manager = self,
|
.manager = self,
|
||||||
.source = source,
|
.source = source,
|
||||||
.script_element = script_element,
|
.script_element = script_element,
|
||||||
@@ -270,7 +272,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
if (is_blocking == false) {
|
if (is_blocking == false) {
|
||||||
self.scriptList(script).remove(&script.node);
|
self.scriptList(script).remove(&script.node);
|
||||||
}
|
}
|
||||||
script.deinit(true);
|
// Let the outer errdefer handle releasing the arena if client.request fails
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
try self.client.request(.{
|
||||||
@@ -278,7 +280,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.blocking = is_blocking,
|
.blocking = is_blocking,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
@@ -289,6 +291,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
});
|
||||||
|
handover = true;
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
@@ -318,7 +321,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
}
|
}
|
||||||
if (script.status == 0) {
|
if (script.status == 0) {
|
||||||
// an error (that we already logged)
|
// an error (that we already logged)
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +330,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer {
|
defer {
|
||||||
self.is_evaluating = was_evaluating;
|
self.is_evaluating = was_evaluating;
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
return script.eval(page);
|
return script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -359,11 +362,14 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
}
|
}
|
||||||
errdefer _ = self.imported_modules.remove(url);
|
errdefer _ = self.imported_modules.remove(url);
|
||||||
|
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "preloadImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -373,11 +379,7 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.mode = .import,
|
.mode = .import,
|
||||||
};
|
};
|
||||||
|
|
||||||
gop.value_ptr.* = ImportedModule{
|
gop.value_ptr.* = ImportedModule{};
|
||||||
.manager = self,
|
|
||||||
};
|
|
||||||
|
|
||||||
const page = self.page;
|
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
@@ -392,12 +394,18 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.client.request(.{
|
// This seems wrong since we're not dealing with an async import (unlike
|
||||||
|
// getAsyncModule below), but all we're trying to do here is pre-load the
|
||||||
|
// script for execution at some point in the future (when waitForImport is
|
||||||
|
// called).
|
||||||
|
self.async_scripts.append(&script.node);
|
||||||
|
|
||||||
|
self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.notification = page._session.notification,
|
.notification = page._session.notification,
|
||||||
@@ -406,13 +414,10 @@ pub fn preloadImport(self: *ScriptManager, url: [:0]const u8, referrer: []const
|
|||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
// This seems wrong since we're not dealing with an async import (unlike
|
return err;
|
||||||
// getAsyncModule below), but all we're trying to do here is pre-load the
|
};
|
||||||
// script for execution at some point in the future (when waitForImport is
|
|
||||||
// called).
|
|
||||||
self.async_scripts.append(&script.node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
||||||
@@ -433,12 +438,12 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
_ = try client.tick(200);
|
_ = try client.tick(200);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
.done => {
|
.done => |script| {
|
||||||
var shared = false;
|
var shared = false;
|
||||||
const buffer = entry.value_ptr.buffer;
|
const buffer = entry.value_ptr.buffer;
|
||||||
const waiters = entry.value_ptr.waiters;
|
const waiters = entry.value_ptr.waiters;
|
||||||
|
|
||||||
if (waiters == 0) {
|
if (waiters == 1) {
|
||||||
self.imported_modules.removeByPtr(entry.key_ptr);
|
self.imported_modules.removeByPtr(entry.key_ptr);
|
||||||
} else {
|
} else {
|
||||||
shared = true;
|
shared = true;
|
||||||
@@ -447,7 +452,7 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
return .{
|
return .{
|
||||||
.buffer = buffer,
|
.buffer = buffer,
|
||||||
.shared = shared,
|
.shared = shared,
|
||||||
.buffer_pool = &self.buffer_pool,
|
.script = script,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
.err => return error.Failed,
|
.err => return error.Failed,
|
||||||
@@ -456,11 +461,14 @@ pub fn waitForImport(self: *ScriptManager, url: [:0]const u8) !ModuleSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.Callback, cb_data: *anyopaque, referrer: []const u8) !void {
|
||||||
const script = try self.script_pool.create();
|
const page = self.page;
|
||||||
errdefer self.script_pool.destroy(script);
|
const arena = try page.getArena(.{ .debug = "getAsyncImport" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const script = try arena.create(Script);
|
||||||
script.* = .{
|
script.* = .{
|
||||||
.kind = .module,
|
.kind = .module,
|
||||||
|
.arena = arena,
|
||||||
.url = url,
|
.url = url,
|
||||||
.node = .{},
|
.node = .{},
|
||||||
.manager = self,
|
.manager = self,
|
||||||
@@ -473,7 +481,6 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
} },
|
} },
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = self.page;
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
var ls: js.Local.Scope = undefined;
|
var ls: js.Local.Scope = undefined;
|
||||||
page.js.localScope(&ls);
|
page.js.localScope(&ls);
|
||||||
@@ -496,11 +503,12 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
self.is_evaluating = true;
|
self.is_evaluating = true;
|
||||||
defer self.is_evaluating = was_evaluating;
|
defer self.is_evaluating = was_evaluating;
|
||||||
|
|
||||||
try self.client.request(.{
|
self.async_scripts.append(&script.node);
|
||||||
|
self.client.request(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.frame_id = page._frame_id,
|
.frame_id = page._frame_id,
|
||||||
.headers = try self.getHeaders(url),
|
.headers = try self.getHeaders(arena, url),
|
||||||
.ctx = script,
|
.ctx = script,
|
||||||
.resource_type = .script,
|
.resource_type = .script,
|
||||||
.cookie_jar = &page._session.cookie_jar,
|
.cookie_jar = &page._session.cookie_jar,
|
||||||
@@ -510,9 +518,10 @@ pub fn getAsyncImport(self: *ScriptManager, url: [:0]const u8, cb: ImportAsync.C
|
|||||||
.data_callback = Script.dataCallback,
|
.data_callback = Script.dataCallback,
|
||||||
.done_callback = Script.doneCallback,
|
.done_callback = Script.doneCallback,
|
||||||
.error_callback = Script.errorCallback,
|
.error_callback = Script.errorCallback,
|
||||||
});
|
}) catch |err| {
|
||||||
|
self.async_scripts.remove(&script.node);
|
||||||
self.async_scripts.append(&script.node);
|
return err;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
// Called from the Page to let us know it's done parsing the HTML. Necessary that
|
||||||
@@ -537,18 +546,18 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
var script: *Script = @fieldParentPtr("node", n);
|
var script: *Script = @fieldParentPtr("node", n);
|
||||||
switch (script.mode) {
|
switch (script.mode) {
|
||||||
.async => {
|
.async => {
|
||||||
defer script.deinit(true);
|
defer script.deinit();
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
},
|
},
|
||||||
.import_async => |ia| {
|
.import_async => |ia| {
|
||||||
defer script.deinit(false);
|
|
||||||
if (script.status < 200 or script.status > 299) {
|
if (script.status < 200 or script.status > 299) {
|
||||||
|
script.deinit();
|
||||||
ia.callback(ia.data, error.FailedToLoad);
|
ia.callback(ia.data, error.FailedToLoad);
|
||||||
} else {
|
} else {
|
||||||
ia.callback(ia.data, .{
|
ia.callback(ia.data, .{
|
||||||
.shared = false,
|
.shared = false,
|
||||||
|
.script = script,
|
||||||
.buffer = script.source.remote,
|
.buffer = script.source.remote,
|
||||||
.buffer_pool = &self.buffer_pool,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -574,7 +583,7 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
}
|
}
|
||||||
defer {
|
defer {
|
||||||
_ = self.defer_scripts.popFirst();
|
_ = self.defer_scripts.popFirst();
|
||||||
script.deinit(true);
|
script.deinit();
|
||||||
}
|
}
|
||||||
script.eval(page);
|
script.eval(page);
|
||||||
}
|
}
|
||||||
@@ -625,11 +634,12 @@ fn parseImportmap(self: *ScriptManager, script: *const Script) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const Script = struct {
|
pub const Script = struct {
|
||||||
complete: bool,
|
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
complete: bool,
|
||||||
status: u16 = 0,
|
status: u16 = 0,
|
||||||
source: Source,
|
source: Source,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
|
arena: Allocator,
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
node: std.DoublyLinkedList.Node,
|
node: std.DoublyLinkedList.Node,
|
||||||
script_element: ?*Element.Html.Script,
|
script_element: ?*Element.Html.Script,
|
||||||
@@ -680,11 +690,8 @@ pub const Script = struct {
|
|||||||
import_async: ImportAsync,
|
import_async: ImportAsync,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deinit(self: *Script, comptime release_buffer: bool) void {
|
fn deinit(self: *Script) void {
|
||||||
if ((comptime release_buffer) and self.source == .remote) {
|
self.manager.page.releaseArena(self.arena);
|
||||||
self.manager.buffer_pool.release(self.source.remote);
|
|
||||||
}
|
|
||||||
self.manager.script_pool.destroy(self);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
@@ -750,9 +757,9 @@ pub const Script = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
lp.assert(self.source.remote.capacity == 0, "ScriptManager.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||||
var buffer = self.manager.buffer_pool.get();
|
var buffer: std.ArrayList(u8) = .empty;
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try buffer.ensureTotalCapacity(self.manager.allocator, cl);
|
try buffer.ensureTotalCapacity(self.arena, cl);
|
||||||
}
|
}
|
||||||
self.source = .{ .remote = buffer };
|
self.source = .{ .remote = buffer };
|
||||||
return true;
|
return true;
|
||||||
@@ -766,7 +773,7 @@ pub const Script = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
try self.source.remote.appendSlice(self.manager.allocator, data);
|
try self.source.remote.appendSlice(self.arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn doneCallback(ctx: *anyopaque) !void {
|
fn doneCallback(ctx: *anyopaque) !void {
|
||||||
@@ -783,9 +790,8 @@ pub const Script = struct {
|
|||||||
} else if (self.mode == .import) {
|
} else if (self.mode == .import) {
|
||||||
manager.async_scripts.remove(&self.node);
|
manager.async_scripts.remove(&self.node);
|
||||||
const entry = manager.imported_modules.getPtr(self.url).?;
|
const entry = manager.imported_modules.getPtr(self.url).?;
|
||||||
entry.state = .done;
|
entry.state = .{ .done = self };
|
||||||
entry.buffer = self.source.remote;
|
entry.buffer = self.source.remote;
|
||||||
self.deinit(false);
|
|
||||||
}
|
}
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
@@ -811,7 +817,7 @@ pub const Script = struct {
|
|||||||
const manager = self.manager;
|
const manager = self.manager;
|
||||||
manager.scriptList(self).remove(&self.node);
|
manager.scriptList(self).remove(&self.node);
|
||||||
if (manager.shutdown) {
|
if (manager.shutdown) {
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +829,7 @@ pub const Script = struct {
|
|||||||
},
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
self.deinit(true);
|
self.deinit();
|
||||||
manager.evaluate();
|
manager.evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,76 +957,6 @@ pub const Script = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BufferPool = struct {
|
|
||||||
count: usize,
|
|
||||||
available: List = .{},
|
|
||||||
allocator: Allocator,
|
|
||||||
max_concurrent_transfers: u8,
|
|
||||||
mem_pool: std.heap.MemoryPool(Container),
|
|
||||||
|
|
||||||
const List = std.SinglyLinkedList;
|
|
||||||
|
|
||||||
const Container = struct {
|
|
||||||
node: List.Node,
|
|
||||||
buf: std.ArrayList(u8),
|
|
||||||
};
|
|
||||||
|
|
||||||
fn init(allocator: Allocator, max_concurrent_transfers: u8) BufferPool {
|
|
||||||
return .{
|
|
||||||
.available = .{},
|
|
||||||
.count = 0,
|
|
||||||
.allocator = allocator,
|
|
||||||
.max_concurrent_transfers = max_concurrent_transfers,
|
|
||||||
.mem_pool = std.heap.MemoryPool(Container).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: *BufferPool) void {
|
|
||||||
const allocator = self.allocator;
|
|
||||||
|
|
||||||
var node = self.available.first;
|
|
||||||
while (node) |n| {
|
|
||||||
const container: *Container = @fieldParentPtr("node", n);
|
|
||||||
container.buf.deinit(allocator);
|
|
||||||
node = n.next;
|
|
||||||
}
|
|
||||||
self.mem_pool.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(self: *BufferPool) std.ArrayList(u8) {
|
|
||||||
const node = self.available.popFirst() orelse {
|
|
||||||
// return a new buffer
|
|
||||||
return .{};
|
|
||||||
};
|
|
||||||
|
|
||||||
self.count -= 1;
|
|
||||||
const container: *Container = @fieldParentPtr("node", node);
|
|
||||||
defer self.mem_pool.destroy(container);
|
|
||||||
return container.buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release(self: *BufferPool, buffer: ArrayList(u8)) void {
|
|
||||||
// create mutable copy
|
|
||||||
var b = buffer;
|
|
||||||
|
|
||||||
if (self.count == self.max_concurrent_transfers) {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = self.mem_pool.create() catch |err| {
|
|
||||||
b.deinit(self.allocator);
|
|
||||||
log.err(.http, "SM BufferPool release", .{ .err = err });
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
b.clearRetainingCapacity();
|
|
||||||
container.* = .{ .buf = b, .node = .{} };
|
|
||||||
self.count += 1;
|
|
||||||
self.available.prepend(&container.node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImportAsync = struct {
|
const ImportAsync = struct {
|
||||||
data: *anyopaque,
|
data: *anyopaque,
|
||||||
callback: ImportAsync.Callback,
|
callback: ImportAsync.Callback,
|
||||||
@@ -1030,12 +966,12 @@ const ImportAsync = struct {
|
|||||||
|
|
||||||
pub const ModuleSource = struct {
|
pub const ModuleSource = struct {
|
||||||
shared: bool,
|
shared: bool,
|
||||||
buffer_pool: *BufferPool,
|
script: *Script,
|
||||||
buffer: std.ArrayList(u8),
|
buffer: std.ArrayList(u8),
|
||||||
|
|
||||||
pub fn deinit(self: *ModuleSource) void {
|
pub fn deinit(self: *ModuleSource) void {
|
||||||
if (self.shared == false) {
|
if (self.shared == false) {
|
||||||
self.buffer_pool.release(self.buffer);
|
self.script.deinit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,15 +981,14 @@ pub const ModuleSource = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ImportedModule = struct {
|
const ImportedModule = struct {
|
||||||
manager: *ScriptManager,
|
waiters: u16 = 1,
|
||||||
state: State = .loading,
|
state: State = .loading,
|
||||||
buffer: std.ArrayList(u8) = .{},
|
buffer: std.ArrayList(u8) = .{},
|
||||||
waiters: u16 = 1,
|
|
||||||
|
|
||||||
const State = enum {
|
const State = union(enum) {
|
||||||
err,
|
err,
|
||||||
done,
|
|
||||||
loading,
|
loading,
|
||||||
|
done: *Script,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// scheduler.run could trigger new http transfers, so do not
|
// scheduler.run could trigger new http transfers, so do not
|
||||||
// store http_client.active BEFORE this call and then use
|
// store http_client.active BEFORE this call and then use
|
||||||
// it AFTER.
|
// it AFTER.
|
||||||
const ms_to_next_task = try browser.runMacrotasks();
|
try browser.runMacrotasks();
|
||||||
|
|
||||||
// Each call to this runs scheduled load events.
|
// Each call to this runs scheduled load events.
|
||||||
try page.dispatchLoad();
|
try page.dispatchLoad();
|
||||||
@@ -423,16 +423,16 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
std.debug.assert(http_client.intercepted == 0);
|
std.debug.assert(http_client.intercepted == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ms: u64 = ms_to_next_task orelse blk: {
|
var ms = blk: {
|
||||||
if (wait_ms - ms_remaining < 100) {
|
// if (wait_ms - ms_remaining < 100) {
|
||||||
if (comptime builtin.is_test) {
|
// if (comptime builtin.is_test) {
|
||||||
return .done;
|
// return .done;
|
||||||
}
|
// }
|
||||||
// Look, we want to exit ASAP, but we don't want
|
// // Look, we want to exit ASAP, but we don't want
|
||||||
// to exit so fast that we've run none of the
|
// // to exit so fast that we've run none of the
|
||||||
// background jobs.
|
// // background jobs.
|
||||||
break :blk 50;
|
// break :blk 50;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (browser.hasBackgroundTasks()) {
|
if (browser.hasBackgroundTasks()) {
|
||||||
// _we_ have nothing to run, but v8 is working on
|
// _we_ have nothing to run, but v8 is working on
|
||||||
@@ -441,9 +441,7 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
break :blk 20;
|
break :blk 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No http transfers, no cdp extra socket, no
|
break :blk browser.msToNextMacrotask() orelse return .done;
|
||||||
// scheduled tasks, we're done.
|
|
||||||
return .done;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ms > ms_remaining) {
|
if (ms > ms_remaining) {
|
||||||
@@ -470,9 +468,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// We're here because we either have active HTTP
|
// We're here because we either have active HTTP
|
||||||
// connections, or exit_when_done == false (aka, there's
|
// connections, or exit_when_done == false (aka, there's
|
||||||
// an cdp_socket registered with the http client).
|
// an cdp_socket registered with the http client).
|
||||||
// We should continue to run lowPriority tasks, so we
|
// We should continue to run tasks, so we minimize how long
|
||||||
// minimize how long we'll poll for network I/O.
|
// we'll poll for network I/O.
|
||||||
var ms_to_wait = @min(200, ms_to_next_task orelse 200);
|
var ms_to_wait = @min(200, browser.msToNextMacrotask() orelse 200);
|
||||||
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
if (ms_to_wait > 10 and browser.hasBackgroundTasks()) {
|
||||||
// if we have background tasks, we don't want to wait too
|
// if we have background tasks, we don't want to wait too
|
||||||
// long for a message from the client. We want to go back
|
// long for a message from the client. We want to go back
|
||||||
@@ -548,7 +546,9 @@ fn processQueuedNavigation(self: *Session) !void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.processFrameNavigation(page, qn);
|
self.processFrameNavigation(page, qn) catch |err| {
|
||||||
|
log.warn(.page, "frame navigation", .{ .url = qn.url, .err = err });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the queue after first pass
|
// Clear the queue after first pass
|
||||||
@@ -588,7 +588,8 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
|
|||||||
|
|
||||||
errdefer iframe._window = null;
|
errdefer iframe._window = null;
|
||||||
|
|
||||||
if (page._parent_notified) {
|
const parent_notified = page._parent_notified;
|
||||||
|
if (parent_notified) {
|
||||||
// we already notified the parent that we had loaded
|
// we already notified the parent that we had loaded
|
||||||
parent._pending_loads += 1;
|
parent._pending_loads += 1;
|
||||||
}
|
}
|
||||||
@@ -598,7 +599,19 @@ fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !v
|
|||||||
page.* = undefined;
|
page.* = undefined;
|
||||||
|
|
||||||
try Page.init(page, frame_id, self, parent);
|
try Page.init(page, frame_id, self, parent);
|
||||||
errdefer page.deinit(true);
|
errdefer {
|
||||||
|
for (parent.frames.items, 0..) |frame, i| {
|
||||||
|
if (frame == page) {
|
||||||
|
parent.frames_sorted = false;
|
||||||
|
_ = parent.frames.swapRemove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent_notified) {
|
||||||
|
parent._pending_loads -= 1;
|
||||||
|
}
|
||||||
|
page.deinit(true);
|
||||||
|
}
|
||||||
|
|
||||||
page.iframe = iframe;
|
page.iframe = iframe;
|
||||||
iframe._window = page.window;
|
iframe._window = page.window;
|
||||||
|
|||||||
104
src/browser/actions.zig
Normal file
104
src/browser/actions.zig
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const lp = @import("../lightpanda.zig");
|
||||||
|
const DOMNode = @import("webapi/Node.zig");
|
||||||
|
const Element = @import("webapi/Element.zig");
|
||||||
|
const Event = @import("webapi/Event.zig");
|
||||||
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
|
const Page = @import("Page.zig");
|
||||||
|
|
||||||
|
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
const mouse_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = true,
|
||||||
|
.composed = true,
|
||||||
|
.clientX = 0,
|
||||||
|
.clientY = 0,
|
||||||
|
}, page);
|
||||||
|
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), mouse_event.asEvent()) catch |err| {
|
||||||
|
lp.log.err(.app, "click failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
|
||||||
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (el.is(Element.Html.Input)) |input| {
|
||||||
|
input.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill input failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.TextArea)) |textarea| {
|
||||||
|
textarea.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill textarea failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else if (el.is(Element.Html.Select)) |select| {
|
||||||
|
select.setValue(text, page) catch |err| {
|
||||||
|
lp.log.err(.app, "fill select failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return error.InvalidNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
|
||||||
|
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
||||||
|
if (node) |n| {
|
||||||
|
const el = n.is(Element) orelse return error.InvalidNodeType;
|
||||||
|
|
||||||
|
if (x) |val| {
|
||||||
|
el.setScrollLeft(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollLeft failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (y) |val| {
|
||||||
|
el.setScrollTop(val, page) catch |err| {
|
||||||
|
lp.log.err(.app, "setScrollTop failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, page);
|
||||||
|
page._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| {
|
||||||
|
lp.log.err(.app, "dispatch scroll event failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
page.window.scrollTo(.{ .x = x orelse 0 }, y, page) catch |err| {
|
||||||
|
lp.log.err(.app, "scroll failed", .{ .err = err });
|
||||||
|
return error.ActionFailed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -253,17 +253,52 @@ pub fn classifyInteractivity(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isInteractiveRole(role: []const u8) bool {
|
pub fn isInteractiveRole(role: []const u8) bool {
|
||||||
const interactive_roles = [_][]const u8{
|
const MAX_LEN = "menuitemcheckbox".len;
|
||||||
"button", "link", "tab", "menuitem",
|
if (role.len > MAX_LEN) return false;
|
||||||
"menuitemcheckbox", "menuitemradio", "switch", "checkbox",
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
"radio", "slider", "spinbutton", "searchbox",
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
"combobox", "option", "treeitem",
|
const interactive_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
};
|
.{ "button", {} },
|
||||||
for (interactive_roles) |r| {
|
.{ "checkbox", {} },
|
||||||
if (std.ascii.eqlIgnoreCase(role, r)) return true;
|
.{ "combobox", {} },
|
||||||
}
|
.{ "iframe", {} },
|
||||||
return false;
|
.{ "link", {} },
|
||||||
|
.{ "listbox", {} },
|
||||||
|
.{ "menuitem", {} },
|
||||||
|
.{ "menuitemcheckbox", {} },
|
||||||
|
.{ "menuitemradio", {} },
|
||||||
|
.{ "option", {} },
|
||||||
|
.{ "radio", {} },
|
||||||
|
.{ "searchbox", {} },
|
||||||
|
.{ "slider", {} },
|
||||||
|
.{ "spinbutton", {} },
|
||||||
|
.{ "switch", {} },
|
||||||
|
.{ "tab", {} },
|
||||||
|
.{ "textbox", {} },
|
||||||
|
.{ "treeitem", {} },
|
||||||
|
});
|
||||||
|
return interactive_roles.has(lowered);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isContentRole(role: []const u8) bool {
|
||||||
|
const MAX_LEN = "columnheader".len;
|
||||||
|
if (role.len > MAX_LEN) return false;
|
||||||
|
var buf: [MAX_LEN]u8 = undefined;
|
||||||
|
const lowered = std.ascii.lowerString(&buf, role);
|
||||||
|
const content_roles = std.StaticStringMap(void).initComptime(.{
|
||||||
|
.{ "article", {} },
|
||||||
|
.{ "cell", {} },
|
||||||
|
.{ "columnheader", {} },
|
||||||
|
.{ "gridcell", {} },
|
||||||
|
.{ "heading", {} },
|
||||||
|
.{ "listitem", {} },
|
||||||
|
.{ "main", {} },
|
||||||
|
.{ "navigation", {} },
|
||||||
|
.{ "region", {} },
|
||||||
|
.{ "rowheader", {} },
|
||||||
|
});
|
||||||
|
return content_roles.has(lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getRole(el: *Element) ?[]const u8 {
|
fn getRole(el: *Element) ?[]const u8 {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ prev_context: *Context,
|
|||||||
|
|
||||||
// Takes the raw v8 isolate and extracts the context from it.
|
// Takes the raw v8 isolate and extracts the context from it.
|
||||||
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void {
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });
|
||||||
initWithContext(self, Context.fromC(v8_context), v8_context);
|
initWithContext(self, ctx, v8_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void {
|
||||||
@@ -537,9 +537,7 @@ pub const Function = struct {
|
|||||||
|
|
||||||
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void {
|
||||||
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?;
|
||||||
const v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?;
|
const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate });
|
||||||
|
|
||||||
const ctx = Context.fromC(v8_context);
|
|
||||||
const info = FunctionCallbackInfo{ .handle = info_handle };
|
const info = FunctionCallbackInfo{ .handle = info_handle };
|
||||||
|
|
||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
|
|||||||
@@ -119,12 +119,22 @@ const ModuleEntry = struct {
|
|||||||
resolver_promise: ?js.Promise.Global = null,
|
resolver_promise: ?js.Promise.Global = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn fromC(c_context: *const v8.Context) *Context {
|
pub fn fromC(c_context: *const v8.Context) ?*Context {
|
||||||
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fromIsolate(isolate: js.Isolate) *Context {
|
/// Returns the Context and v8::Context for the given isolate.
|
||||||
return fromC(v8.v8__Isolate__GetCurrentContext(isolate.handle).?);
|
/// If the current context is from a destroyed Context (e.g., navigated-away iframe),
|
||||||
|
/// falls back to the incumbent context (the calling context).
|
||||||
|
pub fn fromIsolate(isolate: js.Isolate) struct { *Context, *const v8.Context } {
|
||||||
|
const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?;
|
||||||
|
if (fromC(v8_context)) |ctx| {
|
||||||
|
return .{ ctx, v8_context };
|
||||||
|
}
|
||||||
|
// The current context's Context struct has been freed (e.g., iframe navigated away).
|
||||||
|
// Fall back to the incumbent context (the calling context).
|
||||||
|
const v8_incumbent = v8.v8__Isolate__GetIncumbentContext(isolate.handle).?;
|
||||||
|
return .{ fromC(v8_incumbent).?, v8_incumbent };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Context) void {
|
pub fn deinit(self: *Context) void {
|
||||||
@@ -155,6 +165,11 @@ pub fn deinit(self: *Context) void {
|
|||||||
|
|
||||||
self.session.releaseOrigin(self.origin);
|
self.session.releaseOrigin(self.origin);
|
||||||
|
|
||||||
|
// Clear the embedder data so that if V8 keeps this context alive
|
||||||
|
// (because objects created in it are still referenced), we don't
|
||||||
|
// have a dangling pointer to our freed Context struct.
|
||||||
|
v8.v8__Context__SetAlignedPointerInEmbedderData(entered.handle, 1, null);
|
||||||
|
|
||||||
v8.v8__Global__Reset(&self.handle);
|
v8.v8__Global__Reset(&self.handle);
|
||||||
env.isolate.notifyContextDisposed();
|
env.isolate.notifyContextDisposed();
|
||||||
// There can be other tasks associated with this context that we need to
|
// There can be other tasks associated with this context that we need to
|
||||||
@@ -167,12 +182,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
|
|||||||
const env = self.env;
|
const env = self.env;
|
||||||
const isolate = env.isolate;
|
const isolate = env.isolate;
|
||||||
|
|
||||||
|
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
||||||
|
|
||||||
const origin = try self.session.getOrCreateOrigin(key);
|
const origin = try self.session.getOrCreateOrigin(key);
|
||||||
errdefer self.session.releaseOrigin(origin);
|
errdefer self.session.releaseOrigin(origin);
|
||||||
|
try origin.takeover(self.origin);
|
||||||
try self.origin.transferTo(origin);
|
|
||||||
lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc });
|
|
||||||
self.origin.deinit(env.app);
|
|
||||||
|
|
||||||
self.origin = origin;
|
self.origin = origin;
|
||||||
|
|
||||||
@@ -197,18 +211,20 @@ pub fn trackTemp(self: *Context, global: v8.Global) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn weakRef(self: *Context, obj: anytype) void {
|
pub fn weakRef(self: *Context, obj: anytype) void {
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
const resolved = js.Local.resolveValue(obj);
|
||||||
|
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
const resolved = js.Local.resolveValue(obj);
|
||||||
|
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
@@ -216,11 +232,12 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
v8.v8__Global__ClearWeak(&fc.global);
|
v8.v8__Global__ClearWeak(&fc.global);
|
||||||
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
|
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn strongRef(self: *Context, obj: anytype) void {
|
pub fn strongRef(self: *Context, obj: anytype) void {
|
||||||
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
const resolved = js.Local.resolveValue(obj);
|
||||||
|
const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
// should not be possible
|
// should not be possible
|
||||||
std.debug.assert(false);
|
std.debug.assert(false);
|
||||||
@@ -252,6 +269,10 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type
|
|||||||
return l.toLocal(global);
|
return l.toLocal(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getIncumbent(self: *Context) *Page {
|
||||||
|
return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?.page;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stringToPersistedFunction(
|
pub fn stringToPersistedFunction(
|
||||||
self: *Context,
|
self: *Context,
|
||||||
function_body: []const u8,
|
function_body: []const u8,
|
||||||
@@ -303,15 +324,15 @@ pub fn module(self: *Context, comptime want_result: bool, local: *const js.Local
|
|||||||
}
|
}
|
||||||
|
|
||||||
const owned_url = try arena.dupeZ(u8, url);
|
const owned_url = try arena.dupeZ(u8, url);
|
||||||
|
if (cacheable and !gop.found_existing) {
|
||||||
|
gop.key_ptr.* = owned_url;
|
||||||
|
}
|
||||||
const m = try compileModule(local, src, owned_url);
|
const m = try compileModule(local, src, owned_url);
|
||||||
|
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
// compileModule is synchronous - nothing can modify the cache during compilation
|
// compileModule is synchronous - nothing can modify the cache during compilation
|
||||||
lp.assert(gop.value_ptr.module == null, "Context.module has module", .{});
|
lp.assert(gop.value_ptr.module == null, "Context.module has module", .{});
|
||||||
gop.value_ptr.module = try m.persist();
|
gop.value_ptr.module = try m.persist();
|
||||||
if (!gop.found_existing) {
|
|
||||||
gop.key_ptr.* = owned_url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break :blk .{ m, owned_url };
|
break :blk .{ m, owned_url };
|
||||||
@@ -473,7 +494,7 @@ fn resolveModuleCallback(
|
|||||||
) callconv(.c) ?*const v8.Module {
|
) callconv(.c) ?*const v8.Module {
|
||||||
_ = import_attributes;
|
_ = import_attributes;
|
||||||
|
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
@@ -506,7 +527,7 @@ pub fn dynamicModuleCallback(
|
|||||||
_ = host_defined_options;
|
_ = host_defined_options;
|
||||||
_ = import_attrs;
|
_ = import_attrs;
|
||||||
|
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
@@ -524,13 +545,13 @@ pub fn dynamicModuleCallback(
|
|||||||
|
|
||||||
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
break :blk js.String.toSliceZ(.{ .local = &local, .handle = resource_name.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
const specifier = js.String.toSliceZ(.{ .local = &local, .handle = v8_specifier.? }) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
||||||
@@ -539,21 +560,21 @@ pub fn dynamicModuleCallback(
|
|||||||
specifier,
|
specifier,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
||||||
return @constCast((local.rejectPromise("Out of memory") catch return null).handle);
|
return @constCast(local.rejectPromise(.{ .generic_error = "Out of memory" }).handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
const promise = self._dynamicModuleCallback(normalized_specifier, resource, &local) catch |err| blk: {
|
||||||
log.err(.js, "dynamic module callback", .{
|
log.err(.js, "dynamic module callback", .{
|
||||||
.err = err,
|
.err = err,
|
||||||
});
|
});
|
||||||
break :blk local.rejectPromise("Failed to load module") catch return null;
|
break :blk local.rejectPromise(.{ .generic_error = "Out of memory" });
|
||||||
};
|
};
|
||||||
return @constCast(promise.handle);
|
return @constCast(promise.handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {
|
pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void {
|
||||||
// @HandleScope implement this without a fat context/local..
|
// @HandleScope implement this without a fat context/local..
|
||||||
const self = fromC(c_context.?);
|
const self = fromC(c_context.?).?;
|
||||||
var local = js.Local{
|
var local = js.Local{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.handle = c_context.?,
|
.handle = c_context.?,
|
||||||
|
|||||||
@@ -382,8 +382,7 @@ pub fn runMicrotasks(self: *Env) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runMacrotasks(self: *Env) !?u64 {
|
pub fn runMacrotasks(self: *Env) !void {
|
||||||
var ms_to_next_task: ?u64 = null;
|
|
||||||
for (self.contexts[0..self.context_count]) |ctx| {
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
// I hate this comptime check as much as you do. But we have tests
|
// I hate this comptime check as much as you do. But we have tests
|
||||||
@@ -398,13 +397,17 @@ pub fn runMacrotasks(self: *Env) !?u64 {
|
|||||||
var hs: js.HandleScope = undefined;
|
var hs: js.HandleScope = undefined;
|
||||||
const entered = ctx.enter(&hs);
|
const entered = ctx.enter(&hs);
|
||||||
defer entered.exit();
|
defer entered.exit();
|
||||||
|
try ctx.scheduler.run();
|
||||||
const ms = (try ctx.scheduler.run()) orelse continue;
|
|
||||||
if (ms_to_next_task == null or ms < ms_to_next_task.?) {
|
|
||||||
ms_to_next_task = ms;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ms_to_next_task;
|
}
|
||||||
|
|
||||||
|
pub fn msToNextMacrotask(self: *Env) ?u64 {
|
||||||
|
var next_task: u64 = std.math.maxInt(u64);
|
||||||
|
for (self.contexts[0..self.context_count]) |ctx| {
|
||||||
|
const candidate = ctx.scheduler.msToNextHigh() orelse continue;
|
||||||
|
next_task = @min(candidate, next_task);
|
||||||
|
}
|
||||||
|
return if (next_task == std.math.maxInt(u64)) null else next_task;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pumpMessageLoop(self: *const Env) void {
|
pub fn pumpMessageLoop(self: *const Env) void {
|
||||||
@@ -492,20 +495,25 @@ pub fn terminate(self: *const Env) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
||||||
|
const promise_event = v8.v8__PromiseRejectMessage__GetEvent(&message_handle);
|
||||||
|
if (promise_event != v8.kPromiseRejectWithNoHandler and promise_event != v8.kPromiseHandlerAddedAfterReject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
||||||
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
||||||
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
const isolate = js.Isolate{ .handle = v8_isolate };
|
||||||
const ctx = Context.fromIsolate(js_isolate);
|
const ctx, const v8_context = Context.fromIsolate(isolate);
|
||||||
|
|
||||||
const local = js.Local{
|
const local = js.Local{
|
||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.isolate = js_isolate,
|
.isolate = isolate,
|
||||||
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
.handle = v8_context,
|
||||||
.call_arena = ctx.call_arena,
|
.call_arena = ctx.call_arena,
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = ctx.page;
|
const page = ctx.page;
|
||||||
page.window.unhandledPromiseRejection(.{
|
page.window.unhandledPromiseRejection(promise_event == v8.kPromiseRejectWithNoHandler, .{
|
||||||
.local = &local,
|
.local = &local,
|
||||||
.handle = &message_handle,
|
.handle = &message_handle,
|
||||||
}, page) catch |err| {
|
}, page) catch |err| {
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ pub fn createError(self: Isolate, msg: []const u8) *const v8.Value {
|
|||||||
return v8.v8__Exception__Error(message).?;
|
return v8.v8__Exception__Error(message).?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createRangeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__RangeError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createReferenceError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__ReferenceError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn createSyntaxError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
|
const message = self.initStringHandle(msg);
|
||||||
|
return v8.v8__Exception__SyntaxError(message).?;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
pub fn createTypeError(self: Isolate, msg: []const u8) *const v8.Value {
|
||||||
const message = self.initStringHandle(msg);
|
const message = self.initStringHandle(msg);
|
||||||
return v8.v8__Exception__TypeError(message).?;
|
return v8.v8__Exception__TypeError(message).?;
|
||||||
|
|||||||
@@ -1206,9 +1206,15 @@ pub fn stackTrace(self: *const Local) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// == Promise Helpers ==
|
// == Promise Helpers ==
|
||||||
pub fn rejectPromise(self: *const Local, value: anytype) !js.Promise {
|
pub fn rejectPromise(self: *const Local, err: js.PromiseResolver.RejectError) js.Promise {
|
||||||
var resolver = js.PromiseResolver.init(self);
|
var resolver = js.PromiseResolver.init(self);
|
||||||
resolver.reject("Local.rejectPromise", value);
|
resolver.rejectError("Local.rejectPromise", err);
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rejectErrorPromise(self: *const Local, value: js.PromiseResolver.RejectError) !js.Promise {
|
||||||
|
var resolver = js.PromiseResolver.init(self);
|
||||||
|
resolver.rejectError("Local.rejectPromise", value);
|
||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
|||||||
// if v8 hasn't called the finalizer directly itself.
|
// if v8 hasn't called the finalizer directly itself.
|
||||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||||
|
|
||||||
|
taken_over: std.ArrayList(*Origin),
|
||||||
|
|
||||||
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
||||||
const arena = try app.arena_pool.acquire();
|
const arena = try app.arena_pool.acquire();
|
||||||
errdefer app.arena_pool.release(arena);
|
errdefer app.arena_pool.release(arena);
|
||||||
@@ -86,14 +88,19 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
|
|||||||
.rc = 1,
|
.rc = 1,
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.key = owned_key,
|
.key = owned_key,
|
||||||
.globals = .empty,
|
|
||||||
.temps = .empty,
|
.temps = .empty,
|
||||||
|
.globals = .empty,
|
||||||
|
.taken_over = .empty,
|
||||||
.security_token = token_global,
|
.security_token = token_global,
|
||||||
};
|
};
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Origin, app: *App) void {
|
pub fn deinit(self: *Origin, app: *App) void {
|
||||||
|
for (self.taken_over.items) |o| {
|
||||||
|
o.deinit(app);
|
||||||
|
}
|
||||||
|
|
||||||
// Call finalizers before releasing anything
|
// Call finalizers before releasing anything
|
||||||
{
|
{
|
||||||
var it = self.finalizer_callbacks.valueIterator();
|
var it = self.finalizer_callbacks.valueIterator();
|
||||||
@@ -196,42 +203,44 @@ pub fn createFinalizerCallback(
|
|||||||
return fc;
|
return fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transferTo(self: *Origin, dest: *Origin) !void {
|
pub fn takeover(self: *Origin, original: *Origin) !void {
|
||||||
const arena = dest.arena;
|
const arena = self.arena;
|
||||||
|
|
||||||
try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len);
|
try self.globals.ensureUnusedCapacity(arena, original.globals.items.len);
|
||||||
for (self.globals.items) |obj| {
|
for (original.globals.items) |obj| {
|
||||||
dest.globals.appendAssumeCapacity(obj);
|
self.globals.appendAssumeCapacity(obj);
|
||||||
}
|
}
|
||||||
self.globals.clearRetainingCapacity();
|
original.globals.clearRetainingCapacity();
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.temps.ensureUnusedCapacity(arena, self.temps.count());
|
try self.temps.ensureUnusedCapacity(arena, original.temps.count());
|
||||||
var it = self.temps.iterator();
|
var it = original.temps.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.temps.clearRetainingCapacity();
|
original.temps.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.finalizer_callbacks.ensureUnusedCapacity(arena, self.finalizer_callbacks.count());
|
try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count());
|
||||||
var it = self.finalizer_callbacks.iterator();
|
var it = original.finalizer_callbacks.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
kv.value_ptr.*.origin = dest;
|
kv.value_ptr.*.origin = self;
|
||||||
try dest.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.finalizer_callbacks.clearRetainingCapacity();
|
original.finalizer_callbacks.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count());
|
try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count());
|
||||||
var it = self.identity_map.iterator();
|
var it = original.identity_map.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
|
||||||
}
|
}
|
||||||
self.identity_map.clearRetainingCapacity();
|
original.identity_map.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try self.taken_over.append(self.arena, original);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A type that has a finalizer can have its finalizer called one of two ways.
|
// A type that has a finalizer can have its finalizer called one of two ways.
|
||||||
|
|||||||
@@ -18,8 +18,11 @@
|
|||||||
|
|
||||||
const js = @import("js.zig");
|
const js = @import("js.zig");
|
||||||
const v8 = js.v8;
|
const v8 = js.v8;
|
||||||
|
|
||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const DOMException = @import("../webapi/DOMException.zig");
|
||||||
|
|
||||||
const PromiseResolver = @This();
|
const PromiseResolver = @This();
|
||||||
|
|
||||||
local: *const js.Local,
|
local: *const js.Local,
|
||||||
@@ -63,6 +66,43 @@ pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const RejectError = union(enum) {
|
||||||
|
/// Not to be confused with `DOMException`; this is bare `Error`.
|
||||||
|
generic_error: []const u8,
|
||||||
|
range_error: []const u8,
|
||||||
|
reference_error: []const u8,
|
||||||
|
syntax_error: []const u8,
|
||||||
|
type_error: []const u8,
|
||||||
|
/// DOM exceptions are unknown to V8, belongs to web standards.
|
||||||
|
dom_exception: struct { err: anyerror },
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Rejects the promise w/ an error object.
|
||||||
|
pub fn rejectError(
|
||||||
|
self: PromiseResolver,
|
||||||
|
comptime source: []const u8,
|
||||||
|
err: RejectError,
|
||||||
|
) void {
|
||||||
|
const handle = switch (err) {
|
||||||
|
.generic_error => |msg| self.local.isolate.createError(msg),
|
||||||
|
.range_error => |msg| self.local.isolate.createRangeError(msg),
|
||||||
|
.reference_error => |msg| self.local.isolate.createReferenceError(msg),
|
||||||
|
.syntax_error => |msg| self.local.isolate.createSyntaxError(msg),
|
||||||
|
.type_error => |msg| self.local.isolate.createTypeError(msg),
|
||||||
|
// "Exceptional".
|
||||||
|
.dom_exception => |exception| {
|
||||||
|
self._reject(DOMException.fromError(exception.err) orelse unreachable) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectDomException", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self._reject(js.Value{ .handle = handle, .local = self.local }) catch |reject_err| {
|
||||||
|
log.err(.bug, "rejectError", .{ .source = source, .err = reject_err, .persistent = false });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||||
const local = self.local;
|
const local = self.local;
|
||||||
const js_val = try local.zigValueToJs(value, .{});
|
const js_val = try local.zigValueToJs(value, .{});
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ pub fn add(self: *Scheduler, ctx: *anyopaque, cb: Callback, run_in_ms: u32, opts
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: *Scheduler) !?u64 {
|
pub fn run(self: *Scheduler) !void {
|
||||||
_ = try self.runQueue(&self.low_priority);
|
const now = milliTimestamp(.monotonic);
|
||||||
return self.runQueue(&self.high_priority);
|
try self.runQueue(&self.low_priority, now);
|
||||||
|
try self.runQueue(&self.high_priority, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hasReadyTasks(self: *Scheduler) bool {
|
pub fn hasReadyTasks(self: *Scheduler) bool {
|
||||||
@@ -84,16 +85,23 @@ pub fn hasReadyTasks(self: *Scheduler) bool {
|
|||||||
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
return queueuHasReadyTask(&self.low_priority, now) or queueuHasReadyTask(&self.high_priority, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
pub fn msToNextHigh(self: *Scheduler) ?u64 {
|
||||||
if (queue.count() == 0) {
|
const task = self.high_priority.peek() orelse return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = milliTimestamp(.monotonic);
|
const now = milliTimestamp(.monotonic);
|
||||||
|
if (task.run_at <= now) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return @intCast(task.run_at - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runQueue(self: *Scheduler, queue: *Queue, now: u64) !void {
|
||||||
|
if (queue.count() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
while (queue.peek()) |*task_| {
|
while (queue.peek()) |*task_| {
|
||||||
if (task_.run_at > now) {
|
if (task_.run_at > now) {
|
||||||
return @intCast(task_.run_at - now);
|
return;
|
||||||
}
|
}
|
||||||
var task = queue.remove();
|
var task = queue.remove();
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -114,7 +122,7 @@ fn runQueue(self: *Scheduler, queue: *Queue) !?u64 {
|
|||||||
try self.low_priority.add(task);
|
try self.low_priority.add(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
fn queueuHasReadyTask(queue: *Queue, now: u64) bool {
|
||||||
|
|||||||
@@ -725,6 +725,8 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/collections.zig"),
|
@import("../webapi/collections.zig"),
|
||||||
@import("../webapi/Console.zig"),
|
@import("../webapi/Console.zig"),
|
||||||
@import("../webapi/Crypto.zig"),
|
@import("../webapi/Crypto.zig"),
|
||||||
|
@import("../webapi/Permissions.zig"),
|
||||||
|
@import("../webapi/StorageManager.zig"),
|
||||||
@import("../webapi/CSS.zig"),
|
@import("../webapi/CSS.zig"),
|
||||||
@import("../webapi/css/CSSRule.zig"),
|
@import("../webapi/css/CSSRule.zig"),
|
||||||
@import("../webapi/css/CSSRuleList.zig"),
|
@import("../webapi/css/CSSRuleList.zig"),
|
||||||
@@ -848,6 +850,7 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/event/FocusEvent.zig"),
|
@import("../webapi/event/FocusEvent.zig"),
|
||||||
@import("../webapi/event/WheelEvent.zig"),
|
@import("../webapi/event/WheelEvent.zig"),
|
||||||
@import("../webapi/event/TextEvent.zig"),
|
@import("../webapi/event/TextEvent.zig"),
|
||||||
|
@import("../webapi/event/InputEvent.zig"),
|
||||||
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
@import("../webapi/event/PromiseRejectionEvent.zig"),
|
||||||
@import("../webapi/MessageChannel.zig"),
|
@import("../webapi/MessageChannel.zig"),
|
||||||
@import("../webapi/MessagePort.zig"),
|
@import("../webapi/MessagePort.zig"),
|
||||||
|
|||||||
@@ -124,352 +124,362 @@ fn hasVisibleContent(root: *Node) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensureNewline(state: *State, writer: *std.Io.Writer) !void {
|
const Context = struct {
|
||||||
if (!state.last_char_was_newline) {
|
state: State,
|
||||||
try writer.writeByte('\n');
|
writer: *std.Io.Writer,
|
||||||
state.last_char_was_newline = true;
|
page: *Page,
|
||||||
|
|
||||||
|
fn ensureNewline(self: *Context) !void {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
fn render(self: *Context, node: *Node) error{WriteFailed}!void {
|
||||||
|
switch (node._type) {
|
||||||
|
.document, .document_fragment => {
|
||||||
|
try self.renderChildren(node);
|
||||||
|
},
|
||||||
|
.element => |el| {
|
||||||
|
try self.renderElement(el);
|
||||||
|
},
|
||||||
|
.cdata => |cd| {
|
||||||
|
if (node.is(Node.CData.Text)) |_| {
|
||||||
|
var text = cd.getData().str();
|
||||||
|
if (self.state.pre_node) |pre| {
|
||||||
|
if (node.parentNode() == pre and node.nextSibling() == null) {
|
||||||
|
text = std.mem.trimRight(u8, text, " \t\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try self.renderText(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderChildren(self: *Context, parent: *Node) !void {
|
||||||
|
var it = parent.childrenIterator();
|
||||||
|
while (it.next()) |child| {
|
||||||
|
try self.render(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderElement(self: *Context, el: *Element) !void {
|
||||||
|
const tag = el.getTag();
|
||||||
|
|
||||||
|
if (!isVisibleElement(el)) return;
|
||||||
|
|
||||||
|
// --- Opening Tag Logic ---
|
||||||
|
|
||||||
|
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
if (shouldAddSpacing(tag)) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
} else if (tag == .li or tag == .tr) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefixes
|
||||||
|
switch (tag) {
|
||||||
|
.h1 => try self.writer.writeAll("# "),
|
||||||
|
.h2 => try self.writer.writeAll("## "),
|
||||||
|
.h3 => try self.writer.writeAll("### "),
|
||||||
|
.h4 => try self.writer.writeAll("#### "),
|
||||||
|
.h5 => try self.writer.writeAll("##### "),
|
||||||
|
.h6 => try self.writer.writeAll("###### "),
|
||||||
|
.ul => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .unordered, .index = 0 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.ol => {
|
||||||
|
if (self.state.list_depth < self.state.list_stack.len) {
|
||||||
|
self.state.list_stack[self.state.list_depth] = .{ .type = .ordered, .index = 1 };
|
||||||
|
self.state.list_depth += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.li => {
|
||||||
|
const indent = if (self.state.list_depth > 0) self.state.list_depth - 1 else 0;
|
||||||
|
for (0..indent) |_| try self.writer.writeAll(" ");
|
||||||
|
|
||||||
|
if (self.state.list_depth > 0 and self.state.list_stack[self.state.list_depth - 1].type == .ordered) {
|
||||||
|
const current_list = &self.state.list_stack[self.state.list_depth - 1];
|
||||||
|
try self.writer.print("{d}. ", .{current_list.index});
|
||||||
|
current_list.index += 1;
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll("- ");
|
||||||
|
}
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = true;
|
||||||
|
self.state.table_row_index = 0;
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
self.state.table_col_count = 0;
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
// Note: leading pipe handled by previous cell closing or tr opening
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
},
|
||||||
|
.blockquote => {
|
||||||
|
try self.writer.writeAll("> ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.pre => {
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = el.asNode();
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = true;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.hr => {
|
||||||
|
try self.writer.writeAll("---\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.br => {
|
||||||
|
if (self.state.in_table) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
} else {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.img => {
|
||||||
|
try self.writer.writeAll(";
|
||||||
|
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
||||||
|
const absolute_src = URL.resolve(self.page.call_arena, self.page.base(), src, .{ .encode = true }) catch src;
|
||||||
|
try self.writer.writeAll(absolute_src);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.anchor => {
|
||||||
|
const has_content = hasVisibleContent(el.asNode());
|
||||||
|
const label = getAnchorLabel(el);
|
||||||
|
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
||||||
|
|
||||||
|
if (!has_content and label == null and href_raw == null) return;
|
||||||
|
|
||||||
|
const has_block = hasBlockDescendant(el.asNode());
|
||||||
|
const href = if (href_raw) |h| URL.resolve(self.page.call_arena, self.page.base(), h, .{ .encode = true }) catch h else null;
|
||||||
|
|
||||||
|
if (has_block) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
if (href) |h| {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeAll("([](");
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
try self.writer.writeAll("))\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStandaloneAnchor(el)) {
|
||||||
|
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeAll(")\n");
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.writer.writeByte('[');
|
||||||
|
if (has_content) {
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
} else {
|
||||||
|
try self.writer.writeAll(label orelse "");
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("](");
|
||||||
|
if (href) |h| {
|
||||||
|
try self.writer.writeAll(h);
|
||||||
|
}
|
||||||
|
try self.writer.writeByte(')');
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
.input => {
|
||||||
|
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
||||||
|
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
||||||
|
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
||||||
|
try self.writer.writeAll(if (checked) "[x] " else "[ ] ");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Children ---
|
||||||
|
try self.renderChildren(el.asNode());
|
||||||
|
|
||||||
|
// --- Closing Tag Logic ---
|
||||||
|
|
||||||
|
// Suffixes
|
||||||
|
switch (tag) {
|
||||||
|
.pre => {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
try self.writer.writeAll("```\n");
|
||||||
|
self.state.pre_node = null;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.code => {
|
||||||
|
if (self.state.pre_node == null) {
|
||||||
|
try self.writer.writeByte('`');
|
||||||
|
self.state.in_code = false;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.b, .strong => {
|
||||||
|
try self.writer.writeAll("**");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.i, .em => {
|
||||||
|
try self.writer.writeAll("*");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.s, .del => {
|
||||||
|
try self.writer.writeAll("~~");
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
.blockquote => {},
|
||||||
|
.ul, .ol => {
|
||||||
|
if (self.state.list_depth > 0) self.state.list_depth -= 1;
|
||||||
|
},
|
||||||
|
.table => {
|
||||||
|
self.state.in_table = false;
|
||||||
|
},
|
||||||
|
.tr => {
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
if (self.state.table_row_index == 0) {
|
||||||
|
try self.writer.writeByte('|');
|
||||||
|
for (0..self.state.table_col_count) |_| {
|
||||||
|
try self.writer.writeAll("---|");
|
||||||
|
}
|
||||||
|
try self.writer.writeByte('\n');
|
||||||
|
}
|
||||||
|
self.state.table_row_index += 1;
|
||||||
|
self.state.last_char_was_newline = true;
|
||||||
|
},
|
||||||
|
.td, .th => {
|
||||||
|
try self.writer.writeAll(" |");
|
||||||
|
self.state.table_col_count += 1;
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-block newlines
|
||||||
|
if (tag.isBlock() and !self.state.in_table) {
|
||||||
|
try self.ensureNewline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderText(self: *Context, text: []const u8) !void {
|
||||||
|
if (text.len == 0) return;
|
||||||
|
|
||||||
|
if (self.state.pre_node) |_| {
|
||||||
|
try self.writer.writeAll(text);
|
||||||
|
self.state.last_char_was_newline = text[text.len - 1] == '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pure whitespace
|
||||||
|
if (isAllWhitespace(text)) {
|
||||||
|
if (!self.state.last_char_was_newline) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse whitespace
|
||||||
|
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
||||||
|
var first = true;
|
||||||
|
while (it.next()) |word| {
|
||||||
|
if (!first or (!self.state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.escape(word);
|
||||||
|
self.state.last_char_was_newline = false;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle trailing whitespace from the original text
|
||||||
|
if (!first and !self.state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
||||||
|
try self.writer.writeByte(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape(self: *Context, text: []const u8) !void {
|
||||||
|
for (text) |c| {
|
||||||
|
switch (c) {
|
||||||
|
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
||||||
|
try self.writer.writeByte('\\');
|
||||||
|
try self.writer.writeByte(c);
|
||||||
|
},
|
||||||
|
else => try self.writer.writeByte(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) !void {
|
||||||
_ = opts;
|
_ = opts;
|
||||||
var state = State{};
|
var ctx: Context = .{
|
||||||
try render(node, &state, writer, page);
|
.state = .{},
|
||||||
if (!state.last_char_was_newline) {
|
.writer = writer,
|
||||||
|
.page = page,
|
||||||
|
};
|
||||||
|
try ctx.render(node);
|
||||||
|
if (!ctx.state.last_char_was_newline) {
|
||||||
try writer.writeByte('\n');
|
try writer.writeByte('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(node: *Node, state: *State, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
|
|
||||||
switch (node._type) {
|
|
||||||
.document, .document_fragment => {
|
|
||||||
try renderChildren(node, state, writer, page);
|
|
||||||
},
|
|
||||||
.element => |el| {
|
|
||||||
try renderElement(el, state, writer, page);
|
|
||||||
},
|
|
||||||
.cdata => |cd| {
|
|
||||||
if (node.is(Node.CData.Text)) |_| {
|
|
||||||
var text = cd.getData().str();
|
|
||||||
if (state.pre_node) |pre| {
|
|
||||||
if (node.parentNode() == pre and node.nextSibling() == null) {
|
|
||||||
text = std.mem.trimRight(u8, text, " \t\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try renderText(text, state, writer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderChildren(parent: *Node, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
|
||||||
var it = parent.childrenIterator();
|
|
||||||
while (it.next()) |child| {
|
|
||||||
try render(child, state, writer, page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderElement(el: *Element, state: *State, writer: *std.Io.Writer, page: *Page) !void {
|
|
||||||
const tag = el.getTag();
|
|
||||||
|
|
||||||
if (!isVisibleElement(el)) return;
|
|
||||||
|
|
||||||
// --- Opening Tag Logic ---
|
|
||||||
|
|
||||||
// Ensure block elements start on a new line (double newline for paragraphs etc)
|
|
||||||
if (tag.isBlock() and !state.in_table) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
if (shouldAddSpacing(tag)) {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
} else if (tag == .li or tag == .tr) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefixes
|
|
||||||
switch (tag) {
|
|
||||||
.h1 => try writer.writeAll("# "),
|
|
||||||
.h2 => try writer.writeAll("## "),
|
|
||||||
.h3 => try writer.writeAll("### "),
|
|
||||||
.h4 => try writer.writeAll("#### "),
|
|
||||||
.h5 => try writer.writeAll("##### "),
|
|
||||||
.h6 => try writer.writeAll("###### "),
|
|
||||||
.ul => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
state.list_stack[state.list_depth] = .{ .type = .unordered, .index = 0 };
|
|
||||||
state.list_depth += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.ol => {
|
|
||||||
if (state.list_depth < state.list_stack.len) {
|
|
||||||
state.list_stack[state.list_depth] = .{ .type = .ordered, .index = 1 };
|
|
||||||
state.list_depth += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.li => {
|
|
||||||
const indent = if (state.list_depth > 0) state.list_depth - 1 else 0;
|
|
||||||
for (0..indent) |_| try writer.writeAll(" ");
|
|
||||||
|
|
||||||
if (state.list_depth > 0 and state.list_stack[state.list_depth - 1].type == .ordered) {
|
|
||||||
const current_list = &state.list_stack[state.list_depth - 1];
|
|
||||||
try writer.print("{d}. ", .{current_list.index});
|
|
||||||
current_list.index += 1;
|
|
||||||
} else {
|
|
||||||
try writer.writeAll("- ");
|
|
||||||
}
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.table => {
|
|
||||||
state.in_table = true;
|
|
||||||
state.table_row_index = 0;
|
|
||||||
state.table_col_count = 0;
|
|
||||||
},
|
|
||||||
.tr => {
|
|
||||||
state.table_col_count = 0;
|
|
||||||
try writer.writeByte('|');
|
|
||||||
},
|
|
||||||
.td, .th => {
|
|
||||||
// Note: leading pipe handled by previous cell closing or tr opening
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
},
|
|
||||||
.blockquote => {
|
|
||||||
try writer.writeAll("> ");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.pre => {
|
|
||||||
try writer.writeAll("```\n");
|
|
||||||
state.pre_node = el.asNode();
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.code => {
|
|
||||||
if (state.pre_node == null) {
|
|
||||||
try writer.writeByte('`');
|
|
||||||
state.in_code = true;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.b, .strong => {
|
|
||||||
try writer.writeAll("**");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.i, .em => {
|
|
||||||
try writer.writeAll("*");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.s, .del => {
|
|
||||||
try writer.writeAll("~~");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.hr => {
|
|
||||||
try writer.writeAll("---\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.br => {
|
|
||||||
if (state.in_table) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
} else {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.img => {
|
|
||||||
try writer.writeAll(";
|
|
||||||
if (el.getAttributeSafe(comptime .wrap("src"))) |src| {
|
|
||||||
const absolute_src = URL.resolve(page.call_arena, page.base(), src, .{ .encode = true }) catch src;
|
|
||||||
try writer.writeAll(absolute_src);
|
|
||||||
}
|
|
||||||
try writer.writeAll(")");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.anchor => {
|
|
||||||
const has_content = hasVisibleContent(el.asNode());
|
|
||||||
const label = getAnchorLabel(el);
|
|
||||||
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
|
|
||||||
|
|
||||||
if (!has_content and label == null and href_raw == null) return;
|
|
||||||
|
|
||||||
const has_block = hasBlockDescendant(el.asNode());
|
|
||||||
const href = if (href_raw) |h| URL.resolve(page.call_arena, page.base(), h, .{ .encode = true }) catch h else null;
|
|
||||||
|
|
||||||
if (has_block) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
if (href) |h| {
|
|
||||||
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
|
||||||
try writer.writeAll("([](");
|
|
||||||
try writer.writeAll(h);
|
|
||||||
try writer.writeAll("))\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStandaloneAnchor(el)) {
|
|
||||||
if (!state.last_char_was_newline) try writer.writeByte('\n');
|
|
||||||
try writer.writeByte('[');
|
|
||||||
if (has_content) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
} else {
|
|
||||||
try writer.writeAll(label orelse "");
|
|
||||||
}
|
|
||||||
try writer.writeAll("](");
|
|
||||||
if (href) |h| {
|
|
||||||
try writer.writeAll(h);
|
|
||||||
}
|
|
||||||
try writer.writeAll(")\n");
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try writer.writeByte('[');
|
|
||||||
if (has_content) {
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
} else {
|
|
||||||
try writer.writeAll(label orelse "");
|
|
||||||
}
|
|
||||||
try writer.writeAll("](");
|
|
||||||
if (href) |h| {
|
|
||||||
try writer.writeAll(h);
|
|
||||||
}
|
|
||||||
try writer.writeByte(')');
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
.input => {
|
|
||||||
const type_attr = el.getAttributeSafe(comptime .wrap("type")) orelse return;
|
|
||||||
if (std.ascii.eqlIgnoreCase(type_attr, "checkbox")) {
|
|
||||||
const checked = el.getAttributeSafe(comptime .wrap("checked")) != null;
|
|
||||||
try writer.writeAll(if (checked) "[x] " else "[ ] ");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Render Children ---
|
|
||||||
try renderChildren(el.asNode(), state, writer, page);
|
|
||||||
|
|
||||||
// --- Closing Tag Logic ---
|
|
||||||
|
|
||||||
// Suffixes
|
|
||||||
switch (tag) {
|
|
||||||
.pre => {
|
|
||||||
if (!state.last_char_was_newline) {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
try writer.writeAll("```\n");
|
|
||||||
state.pre_node = null;
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.code => {
|
|
||||||
if (state.pre_node == null) {
|
|
||||||
try writer.writeByte('`');
|
|
||||||
state.in_code = false;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.b, .strong => {
|
|
||||||
try writer.writeAll("**");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.i, .em => {
|
|
||||||
try writer.writeAll("*");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.s, .del => {
|
|
||||||
try writer.writeAll("~~");
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
.blockquote => {},
|
|
||||||
.ul, .ol => {
|
|
||||||
if (state.list_depth > 0) state.list_depth -= 1;
|
|
||||||
},
|
|
||||||
.table => {
|
|
||||||
state.in_table = false;
|
|
||||||
},
|
|
||||||
.tr => {
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
if (state.table_row_index == 0) {
|
|
||||||
try writer.writeByte('|');
|
|
||||||
for (0..state.table_col_count) |_| {
|
|
||||||
try writer.writeAll("---|");
|
|
||||||
}
|
|
||||||
try writer.writeByte('\n');
|
|
||||||
}
|
|
||||||
state.table_row_index += 1;
|
|
||||||
state.last_char_was_newline = true;
|
|
||||||
},
|
|
||||||
.td, .th => {
|
|
||||||
try writer.writeAll(" |");
|
|
||||||
state.table_col_count += 1;
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-block newlines
|
|
||||||
if (tag.isBlock() and !state.in_table) {
|
|
||||||
try ensureNewline(state, writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn renderText(text: []const u8, state: *State, writer: *std.Io.Writer) !void {
|
|
||||||
if (text.len == 0) return;
|
|
||||||
|
|
||||||
if (state.pre_node) |_| {
|
|
||||||
try writer.writeAll(text);
|
|
||||||
state.last_char_was_newline = text[text.len - 1] == '\n';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for pure whitespace
|
|
||||||
if (isAllWhitespace(text)) {
|
|
||||||
if (!state.last_char_was_newline) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse whitespace
|
|
||||||
var it = std.mem.tokenizeAny(u8, text, " \t\n\r");
|
|
||||||
var first = true;
|
|
||||||
while (it.next()) |word| {
|
|
||||||
if (!first or (!state.last_char_was_newline and std.ascii.isWhitespace(text[0]))) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
try escapeMarkdown(writer, word);
|
|
||||||
state.last_char_was_newline = false;
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle trailing whitespace from the original text
|
|
||||||
if (!first and !state.last_char_was_newline and std.ascii.isWhitespace(text[text.len - 1])) {
|
|
||||||
try writer.writeByte(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escapeMarkdown(writer: *std.Io.Writer, text: []const u8) !void {
|
|
||||||
for (text) |c| {
|
|
||||||
switch (c) {
|
|
||||||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '#', '+', '-', '!', '|' => {
|
|
||||||
try writer.writeByte('\\');
|
|
||||||
try writer.writeByte(c);
|
|
||||||
},
|
|
||||||
else => try writer.writeByte(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
fn testMarkdownHTML(html: []const u8, expected: []const u8) !void {
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
const page = try testing.test_session.createPage();
|
const page = try testing.test_session.createPage();
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
|
|
||||||
<script id=byId name="test1">
|
<script id=byId name="test1">
|
||||||
testing.expectEqual(1, document.querySelector.length);
|
testing.expectEqual(1, document.querySelector.length);
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelector(''));
|
testing.expectError("SyntaxError", () => document.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => document.querySelector(''));
|
}, () => document.querySelector(''));
|
||||||
|
|
||||||
testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));
|
testing.expectEqual('test1', document.querySelector('#byId').getAttribute('name'));
|
||||||
|
|||||||
@@ -34,11 +34,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id=script1 name="test1">
|
<script id=script1 name="test1">
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => document.querySelectorAll(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => document.querySelectorAll(''));
|
}, () => document.querySelectorAll(''));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
|
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(3, err.code);
|
testing.expectEqual(3, err.code);
|
||||||
testing.expectEqual('Hierarchy Error', err.message);
|
testing.expectEqual('HierarchyRequestError', err.name);
|
||||||
testing.expectEqual(true, err instanceof DOMException);
|
testing.expectEqual(true, err instanceof DOMException);
|
||||||
testing.expectEqual(true, err instanceof Error);
|
testing.expectEqual(true, err instanceof Error);
|
||||||
}, () => link.appendChild(content));
|
}, () => link.appendChild(content));
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Empty XML is a parse error (no root element)
|
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
testing.expectError('Error', () => parser.parseFromString('', 'text/xml'));
|
let d = parser.parseFromString('', 'text/xml');
|
||||||
|
testing.expectEqual('<parsererror>error</parsererror>', new XMLSerializer().serializeToString(d));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => el1.removeAttributeNode(script_id_node));
|
}, () => el1.removeAttributeNode(script_id_node));
|
||||||
|
|
||||||
testing.expectEqual(an1, el1.removeAttributeNode(an1));
|
testing.expectEqual(an1, el1.removeAttributeNode(an1));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
testing.expectEqual('', $('#a0').href);
|
testing.expectEqual('', $('#a0').href);
|
||||||
|
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
testing.expectEqual(testing.BASE_URL + 'element/anchor1.html', $('#a1').href);
|
||||||
testing.expectEqual(testing.ORIGIN + 'hello/world/anchor2.html', $('#a2').href);
|
testing.expectEqual(testing.ORIGIN + '/hello/world/anchor2.html', $('#a2').href);
|
||||||
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
testing.expectEqual('https://www.openmymind.net/Elixirs-With-Statement/', $('#a3').href);
|
||||||
|
|
||||||
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
testing.expectEqual(testing.BASE_URL + 'element/html/foo', $('#link').href);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)
|
testing.expectEqual(testing.BASE_URL + 'element/html/hello', form.action)
|
||||||
|
|
||||||
form.action = '/hello';
|
form.action = '/hello';
|
||||||
testing.expectEqual(testing.ORIGIN + 'hello', form.action)
|
testing.expectEqual(testing.ORIGIN + '/hello', form.action)
|
||||||
|
|
||||||
form.action = 'https://lightpanda.io/hello';
|
form.action = 'https://lightpanda.io/hello';
|
||||||
testing.expectEqual('https://lightpanda.io/hello', form.action)
|
testing.expectEqual('https://lightpanda.io/hello', form.action)
|
||||||
@@ -343,3 +343,123 @@
|
|||||||
testing.expectEqual('', form.elements['choice'].value)
|
testing.expectEqual('', form.elements['choice'].value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() fires the submit event (unlike submit()) -->
|
||||||
|
<form id="test_form2" action="/should-not-navigate2" method="get">
|
||||||
|
<input name="q" value="test2">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_fires_submit_event">
|
||||||
|
{
|
||||||
|
const form = $('#test_form2');
|
||||||
|
let submitFired = false;
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitFired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit();
|
||||||
|
|
||||||
|
testing.expectEqual(true, submitFired);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with preventDefault stops navigation -->
|
||||||
|
<form id="test_form3" action="/should-not-navigate3" method="get">
|
||||||
|
<input name="q" value="test3">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_respects_preventDefault">
|
||||||
|
{
|
||||||
|
const form = $('#test_form3');
|
||||||
|
|
||||||
|
form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
form.requestSubmit();
|
||||||
|
|
||||||
|
// Form submission was prevented, so no navigation should be scheduled
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with non-submit-button submitter throws TypeError -->
|
||||||
|
<form id="test_form_rs1" action="/should-not-navigate4" method="get">
|
||||||
|
<input id="rs1_text" type="text" name="q" value="test">
|
||||||
|
<input id="rs1_submit" type="submit" value="Go">
|
||||||
|
<input id="rs1_image" type="image" src="x.png">
|
||||||
|
<button id="rs1_btn_submit" type="submit">Submit</button>
|
||||||
|
<button id="rs1_btn_reset" type="reset">Reset</button>
|
||||||
|
<button id="rs1_btn_button" type="button">Button</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_rejects_non_submit_button">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs1');
|
||||||
|
form.addEventListener('submit', (e) => e.preventDefault());
|
||||||
|
|
||||||
|
// A text input is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_text'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A reset button is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_btn_reset'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A <button type="button"> is not a submit button — should throw TypeError
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit($('#rs1_btn_button'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A <div> is not a submit button — should throw TypeError
|
||||||
|
const div = document.createElement('div');
|
||||||
|
form.appendChild(div);
|
||||||
|
testing.expectError('TypeError', () => {
|
||||||
|
form.requestSubmit(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() accepts valid submit buttons -->
|
||||||
|
<script id="requestSubmit_accepts_submit_buttons">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs1');
|
||||||
|
let submitCount = 0;
|
||||||
|
form.addEventListener('submit', (e) => { e.preventDefault(); submitCount++; });
|
||||||
|
|
||||||
|
// <input type="submit"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_submit'));
|
||||||
|
testing.expectEqual(1, submitCount);
|
||||||
|
|
||||||
|
// <input type="image"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_image'));
|
||||||
|
testing.expectEqual(2, submitCount);
|
||||||
|
|
||||||
|
// <button type="submit"> is a valid submitter
|
||||||
|
form.requestSubmit($('#rs1_btn_submit'));
|
||||||
|
testing.expectEqual(3, submitCount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Test: requestSubmit() with submitter not owned by form throws NotFoundError -->
|
||||||
|
<form id="test_form_rs2" action="/should-not-navigate5" method="get">
|
||||||
|
<input type="text" name="q" value="test">
|
||||||
|
</form>
|
||||||
|
<form id="test_form_rs3">
|
||||||
|
<input id="rs3_submit" type="submit" value="Other Submit">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script id="requestSubmit_rejects_wrong_form_submitter">
|
||||||
|
{
|
||||||
|
const form = $('#test_form_rs2');
|
||||||
|
|
||||||
|
// Submit button belongs to a different form — should throw NotFoundError
|
||||||
|
testing.expectError('NotFoundError', () => {
|
||||||
|
form.requestSubmit($('#rs3_submit'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
testing.expectEqual('test.png', img.getAttribute('src'));
|
testing.expectEqual('test.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = '/absolute/path.png';
|
img.src = '/absolute/path.png';
|
||||||
testing.expectEqual(testing.ORIGIN + 'absolute/path.png', img.src);
|
testing.expectEqual(testing.ORIGIN + '/absolute/path.png', img.src);
|
||||||
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
testing.expectEqual('/absolute/path.png', img.getAttribute('src'));
|
||||||
|
|
||||||
img.src = 'https://example.com/image.png';
|
img.src = 'https://example.com/image.png';
|
||||||
|
|||||||
@@ -191,14 +191,14 @@
|
|||||||
|
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
let lastEvent = null;
|
let lastEvent = null;
|
||||||
|
|
||||||
input.addEventListener('selectionchange', (e) => {
|
input.addEventListener('selectionchange', (e) => {
|
||||||
eventCount++;
|
eventCount++;
|
||||||
lastEvent = e;
|
lastEvent = e;
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectEqual(0, eventCount);
|
testing.expectEqual(0, eventCount);
|
||||||
|
|
||||||
input.setSelectionRange(0, 5);
|
input.setSelectionRange(0, 5);
|
||||||
input.select();
|
input.select();
|
||||||
input.selectionStart = 3;
|
input.selectionStart = 3;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
testing.expectEqual('https://lightpanda.io/opensource-browser/15', l2.href);
|
||||||
|
|
||||||
l2.href = '/over/9000';
|
l2.href = '/over/9000';
|
||||||
testing.expectEqual(testing.ORIGIN + 'over/9000', l2.href);
|
testing.expectEqual(testing.ORIGIN + '/over/9000', l2.href);
|
||||||
|
|
||||||
l2.crossOrigin = 'nope';
|
l2.crossOrigin = 'nope';
|
||||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
@@ -84,3 +84,24 @@
|
|||||||
testing.eventually(() => testing.expectEqual(true, result));
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="refs">
|
||||||
|
{
|
||||||
|
const rels = ['stylesheet', 'preload', 'modulepreload'];
|
||||||
|
const results = rels.map(() => false);
|
||||||
|
rels.forEach((rel, i) => {
|
||||||
|
let link = document.createElement('link')
|
||||||
|
link.rel = rel;
|
||||||
|
link.href = '/nope';
|
||||||
|
link.onload = () => results[i] = true;
|
||||||
|
document.documentElement.appendChild(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
results.forEach((r) => {
|
||||||
|
testing.expectEqual(true, r);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -66,11 +66,10 @@
|
|||||||
{
|
{
|
||||||
const container = $('#test-container');
|
const container = $('#test-container');
|
||||||
|
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => container.matches(''));
|
testing.expectError("SyntaxError", () => container.matches(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => container.matches(''));
|
}, () => container.matches(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
testing.expectEqual(null, p1.querySelector('#p1'));
|
testing.expectEqual(null, p1.querySelector('#p1'));
|
||||||
|
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => p1.querySelector(''));
|
testing.expectError("SyntaxError", () => p1.querySelector(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => p1.querySelector(''));
|
}, () => p1.querySelector(''));
|
||||||
|
|
||||||
testing.expectEqual($('#c2'), p1.querySelector('#c2'));
|
testing.expectEqual($('#c2'), p1.querySelector('#c2'));
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
<script id=errors>
|
<script id=errors>
|
||||||
{
|
{
|
||||||
const root = $('#root');
|
const root = $('#root');
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => root.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => root.querySelectorAll(''));
|
||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(12, err.code);
|
testing.expectEqual(12, err.code);
|
||||||
testing.expectEqual("SyntaxError", err.name);
|
testing.expectEqual("SyntaxError", err.name);
|
||||||
testing.expectEqual("Syntax Error", err.message);
|
|
||||||
}, () => root.querySelectorAll(''));
|
}, () => root.querySelectorAll(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
const container = $('#container');
|
const container = $('#container');
|
||||||
|
|
||||||
// Empty selectors
|
// Empty selectors
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => container.querySelector(''));
|
testing.expectError("SyntaxError", () => container.querySelector(''));
|
||||||
testing.expectError("SyntaxError: Syntax Error", () => document.querySelectorAll(''));
|
testing.expectError("SyntaxError", () => document.querySelectorAll(''));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
38
src/browser/tests/event/report_error.html
Normal file
38
src/browser/tests/event/report_error.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=onerrorFiveArguments>
|
||||||
|
let called = false;
|
||||||
|
let argCount = 0;
|
||||||
|
window.onerror = function() {
|
||||||
|
called = true;
|
||||||
|
argCount = arguments.length;
|
||||||
|
return true; // suppress default
|
||||||
|
};
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual(true, called);
|
||||||
|
testing.expectEqual(5, argCount);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorCalledBeforeEventListener>
|
||||||
|
let callOrder = [];
|
||||||
|
window.onerror = function() { callOrder.push('onerror'); return true; };
|
||||||
|
window.addEventListener('error', function() { callOrder.push('listener'); });
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual('onerror', callOrder[0]);
|
||||||
|
testing.expectEqual('listener', callOrder[1]);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=onerrorReturnTrueSuppresses>
|
||||||
|
let listenerCalled = false;
|
||||||
|
window.onerror = function() { return true; };
|
||||||
|
window.addEventListener('error', function(e) {
|
||||||
|
// listener still fires even when onerror returns true
|
||||||
|
listenerCalled = true;
|
||||||
|
});
|
||||||
|
try { undefinedVariable; } catch(e) { window.reportError(e); }
|
||||||
|
testing.expectEqual(true, listenerCalled);
|
||||||
|
window.onerror = null;
|
||||||
|
</script>
|
||||||
25
src/browser/tests/frames/post_message.html
Normal file
25
src/browser/tests/frames/post_message.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<iframe id="receiver"></iframe>
|
||||||
|
|
||||||
|
<script id="messages">
|
||||||
|
{
|
||||||
|
let reply = null;
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
console.warn('reply')
|
||||||
|
reply = e.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframe = $('#receiver');
|
||||||
|
iframe.src = 'support/message_receiver.html';
|
||||||
|
iframe.addEventListener('load', () => {
|
||||||
|
iframe.contentWindow.postMessage('ping', '*');
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual('pong', reply.data);
|
||||||
|
testing.expectEqual(testing.ORIGIN, reply.origin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
9
src/browser/tests/frames/support/message_receiver.html
Normal file
9
src/browser/tests/frames/support/message_receiver.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script>
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
console.warn('Frame Message', e.data);
|
||||||
|
if (e.data === 'ping') {
|
||||||
|
window.top.postMessage({data: 'pong', origin: e.origin}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,37 +2,17 @@
|
|||||||
<script src="testing.js"></script>
|
<script src="testing.js"></script>
|
||||||
|
|
||||||
<script id=history>
|
<script id=history>
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
// This test is a bit wonky. But it's trying to test navigation, which is
|
||||||
|
// something we can't do in the main page (we can't navigate away from this
|
||||||
history.scrollRestoration = 'manual';
|
// page and still assertOk in the test runner).
|
||||||
testing.expectEqual('manual', history.scrollRestoration);
|
// If support/history.html has a failed assertion, it'll log the error and
|
||||||
|
// stop the script. If it succeeds, it'll set support_history_completed
|
||||||
history.scrollRestoration = 'auto';
|
// which we can use here to assume everything passed.
|
||||||
testing.expectEqual('auto', history.scrollRestoration);
|
|
||||||
testing.expectEqual(null, history.state)
|
|
||||||
|
|
||||||
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/browser/tests/history_after_nav.skip.html');
|
|
||||||
testing.expectEqual({ testInProgress: true }, history.state);
|
|
||||||
|
|
||||||
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');
|
|
||||||
history.replaceState({ "new": "field", testComplete: true }, null);
|
|
||||||
|
|
||||||
let state = { "new": "field", testComplete: true };
|
|
||||||
testing.expectEqual(state, history.state);
|
|
||||||
|
|
||||||
let popstateEventFired = false;
|
|
||||||
let popstateEventState = null;
|
|
||||||
|
|
||||||
window.addEventListener('popstate', (event) => {
|
|
||||||
popstateEventFired = true;
|
|
||||||
popstateEventState = event.state;
|
|
||||||
});
|
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.eventually(() => {
|
||||||
testing.expectEqual(true, popstateEventFired);
|
testing.expectEqual(true, window.support_history_completed);
|
||||||
testing.expectEqual({testInProgress: true }, popstateEventState);
|
testing.expectEqual(true, window.support_history_popstateEventFired);
|
||||||
})
|
testing.expectEqual({testInProgress: true }, window.support_history_popstateEventState);
|
||||||
|
});
|
||||||
history.back();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<iframe id=frame src="support/history.html"></iframe>
|
||||||
|
|||||||
14
src/browser/tests/mcp_actions.html
Normal file
14
src/browser/tests/mcp_actions.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<button id="btn" onclick="window.clicked = true;">Click Me</button>
|
||||||
|
<input id="inp" oninput="window.inputVal = this.value" onchange="window.changed = true;">
|
||||||
|
<select id="sel" onchange="window.selChanged = this.value">
|
||||||
|
<option value="opt1">Option 1</option>
|
||||||
|
<option value="opt2">Option 2</option>
|
||||||
|
</select>
|
||||||
|
<div id="scrollbox" style="width: 100px; height: 100px; overflow: scroll;" onscroll="window.scrolled = true;">
|
||||||
|
<div style="height: 500px;">Long content</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -27,3 +27,44 @@
|
|||||||
testing.expectEqual(false, navigator.javaEnabled());
|
testing.expectEqual(false, navigator.javaEnabled());
|
||||||
testing.expectEqual(false, navigator.webdriver);
|
testing.expectEqual(false, navigator.webdriver);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=permission_query>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.permissions.query({ name: 'notifications' });
|
||||||
|
testing.expectTrue(p instanceof Promise);
|
||||||
|
const status = await p;
|
||||||
|
restore();
|
||||||
|
testing.expectEqual('prompt', status.state);
|
||||||
|
testing.expectEqual('notifications', status.name);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=storage_estimate>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.storage.estimate();
|
||||||
|
testing.expectTrue(p instanceof Promise);
|
||||||
|
|
||||||
|
const estimate = await p;
|
||||||
|
restore();
|
||||||
|
testing.expectEqual(0, estimate.usage);
|
||||||
|
testing.expectEqual(1024 * 1024 * 1024, estimate.quota);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=deviceMemory>
|
||||||
|
testing.expectEqual(8, navigator.deviceMemory);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=getBattery>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const p = navigator.getBattery();
|
||||||
|
try {
|
||||||
|
await p;
|
||||||
|
testing.fail('getBattery should reject');
|
||||||
|
} catch (err) {
|
||||||
|
restore();
|
||||||
|
testing.expectEqual('NotSupportedError', err.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -203,3 +203,39 @@
|
|||||||
testing.expectEqual(true, response.body !== null);
|
testing.expectEqual(true, response.body !== null);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=fetch_blob_url>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
// Create a blob and get its URL
|
||||||
|
const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const response = await fetch(blobUrl);
|
||||||
|
restore();
|
||||||
|
|
||||||
|
testing.expectEqual(200, response.status);
|
||||||
|
testing.expectEqual(true, response.ok);
|
||||||
|
testing.expectEqual(blobUrl, response.url);
|
||||||
|
testing.expectEqual('text/plain', response.headers.get('Content-Type'));
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
testing.expectEqual('Hello from blob!', text);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=abort>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
controller.abort();
|
||||||
|
try {
|
||||||
|
await fetch('http://127.0.0.1:9582/xhr', { signal: controller.signal });
|
||||||
|
testain.fail('fetch should have been aborted');
|
||||||
|
} catch (e) {
|
||||||
|
restore();
|
||||||
|
testing.expectEqual("AbortError", e.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -283,3 +283,26 @@
|
|||||||
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
testing.expectEqual(XMLHttpRequest.UNSENT, req.readyState);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=xhr_blob_url>
|
||||||
|
testing.async(async (restore) => {
|
||||||
|
// Create a blob and get its URL
|
||||||
|
const blob = new Blob(['Hello from blob!'], { type: 'text/plain' });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
req.onload = resolve;
|
||||||
|
req.open('GET', blobUrl);
|
||||||
|
req.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
restore();
|
||||||
|
testing.expectEqual(200, req.status);
|
||||||
|
testing.expectEqual('Hello from blob!', req.responseText);
|
||||||
|
testing.expectEqual(blobUrl, req.responseURL);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => d1.insertBefore(document.createElement('div'), d2));
|
}, () => d1.insertBefore(document.createElement('div'), d2));
|
||||||
|
|
||||||
let c1 = document.createElement('div');
|
let c1 = document.createElement('div');
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(8, err.code);
|
testing.expectEqual(8, err.code);
|
||||||
testing.expectEqual("NotFoundError", err.name);
|
testing.expectEqual("NotFoundError", err.name);
|
||||||
testing.expectEqual("Not Found", err.message);
|
|
||||||
}, () => $('#d1').removeChild($('#p1')));
|
}, () => $('#d1').removeChild($('#p1')));
|
||||||
|
|
||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
testing.withError((err) => {
|
testing.withError((err) => {
|
||||||
testing.expectEqual(3, err.code);
|
testing.expectEqual(3, err.code);
|
||||||
testing.expectEqual("HierarchyRequestError", err.name);
|
testing.expectEqual("HierarchyRequestError", err.name);
|
||||||
testing.expectEqual("Hierarchy Error", err.message);
|
|
||||||
}, () => d1.replaceChild(c4, c3));
|
}, () => d1.replaceChild(c4, c3));
|
||||||
|
|
||||||
testing.expectEqual(c2, d1.replaceChild(c4, c2));
|
testing.expectEqual(c2, d1.replaceChild(c4, c2));
|
||||||
|
|||||||
@@ -451,12 +451,12 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|
||||||
// Test setStart with offset beyond node length
|
// Test setStart with offset beyond node length
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setStart(p1, 999);
|
range.setStart(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with negative offset (wraps to large u32)
|
// Test with negative offset (wraps to large u32)
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setStart(p1.firstChild, -1);
|
range.setStart(p1.firstChild, -1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -468,12 +468,12 @@
|
|||||||
const p1 = $('#p1');
|
const p1 = $('#p1');
|
||||||
|
|
||||||
// Test setEnd with offset beyond node length
|
// Test setEnd with offset beyond node length
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setEnd(p1, 999);
|
range.setEnd(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with text node
|
// Test with text node
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.setEnd(p1.firstChild, 9999);
|
range.setEnd(p1.firstChild, 9999);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -525,11 +525,11 @@
|
|||||||
range.setEnd(p1, 1);
|
range.setEnd(p1, 1);
|
||||||
|
|
||||||
// Test comparePoint with invalid offset
|
// Test comparePoint with invalid offset
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.comparePoint(p1, 20);
|
range.comparePoint(p1, 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.comparePoint(p1.firstChild, -1);
|
range.comparePoint(p1.firstChild, -1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -650,11 +650,11 @@
|
|||||||
range.setEnd(p1, 1);
|
range.setEnd(p1, 1);
|
||||||
|
|
||||||
// Invalid offset should throw IndexSizeError
|
// Invalid offset should throw IndexSizeError
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.isPointInRange(p1, 999);
|
range.isPointInRange(p1, 999);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('IndexSizeError: Index or size is negative or greater than the allowed amount', () => {
|
testing.expectError('IndexSizeError:', () => {
|
||||||
range.isPointInRange(p1.firstChild, 9999);
|
range.isPointInRange(p1.firstChild, 9999);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -854,11 +854,11 @@
|
|||||||
range2.setStart(p, 0);
|
range2.setStart(p, 0);
|
||||||
|
|
||||||
// Invalid how parameter should throw NotSupportedError
|
// Invalid how parameter should throw NotSupportedError
|
||||||
testing.expectError('NotSupportedError: Not Supported', () => {
|
testing.expectError('NotSupportedError:', () => {
|
||||||
range1.compareBoundaryPoints(4, range2);
|
range1.compareBoundaryPoints(4, range2);
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectError('NotSupportedError: Not Supported', () => {
|
testing.expectError('NotSupportedError:', () => {
|
||||||
range1.compareBoundaryPoints(99, range2);
|
range1.compareBoundaryPoints(99, range2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -883,7 +883,7 @@
|
|||||||
range2.setEnd(foreignP, 1);
|
range2.setEnd(foreignP, 1);
|
||||||
|
|
||||||
// Comparing ranges in different documents should throw WrongDocumentError
|
// Comparing ranges in different documents should throw WrongDocumentError
|
||||||
testing.expectError('WrongDocumentError: wrong_document_error', () => {
|
testing.expectError('WrongDocumentError:', () => {
|
||||||
range1.compareBoundaryPoints(Range.START_TO_START, range2);
|
range1.compareBoundaryPoints(Range.START_TO_START, range2);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div id="host2"></div>
|
<div id="host2"></div>
|
||||||
<div id="host3"></div>
|
<div id="host3"></div>
|
||||||
|
|
||||||
<!-- <script id="attachShadow_open">
|
<script id="attachShadow_open">
|
||||||
{
|
{
|
||||||
const host = $('#host1');
|
const host = $('#host1');
|
||||||
const shadow = host.attachShadow({ mode: 'open' });
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
shadow.replaceChildren('New content');
|
shadow.replaceChildren('New content');
|
||||||
testing.expectEqual('New content', shadow.innerHTML);
|
testing.expectEqual('New content', shadow.innerHTML);
|
||||||
}
|
}
|
||||||
</script> -->
|
</script>
|
||||||
|
|
||||||
<script id="getElementById">
|
<script id="getElementById">
|
||||||
{
|
{
|
||||||
@@ -154,3 +154,16 @@
|
|||||||
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
testing.expectEqual(null, shadow.getElementById('nonexistent'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script id=adoptedStyleSheets>
|
||||||
|
{
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const acss = shadow.adoptedStyleSheets;
|
||||||
|
testing.expectEqual(0, acss.length);
|
||||||
|
acss.push(new CSSStyleSheet());
|
||||||
|
testing.expectEqual(1, acss.length);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
33
src/browser/tests/support/history.html
Normal file
33
src/browser/tests/support/history.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=history>
|
||||||
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
|
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
testing.expectEqual('manual', history.scrollRestoration);
|
||||||
|
|
||||||
|
history.scrollRestoration = 'auto';
|
||||||
|
testing.expectEqual('auto', history.scrollRestoration);
|
||||||
|
testing.expectEqual(null, history.state)
|
||||||
|
|
||||||
|
history.pushState({ testInProgress: true }, null, testing.BASE_URL + 'history_after_nav.skip.html');
|
||||||
|
testing.expectEqual({ testInProgress: true }, history.state);
|
||||||
|
|
||||||
|
history.pushState({ testInProgress: false }, null, testing.ORIGIN + '/xhr/json');
|
||||||
|
history.replaceState({ "new": "field", testComplete: true }, null);
|
||||||
|
|
||||||
|
let state = { "new": "field", testComplete: true };
|
||||||
|
testing.expectEqual(state, history.state);
|
||||||
|
|
||||||
|
let popstateEventFired = false;
|
||||||
|
let popstateEventState = null;
|
||||||
|
|
||||||
|
window.top.support_history_completed = true;
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
window.top.window.support_history_popstateEventFired = true;
|
||||||
|
window.top.window.support_history_popstateEventState = event.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
history.back();
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -99,8 +99,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// our test runner sets this to true
|
const IS_TEST_RUNNER = window.navigator.userAgent.startsWith("Lightpanda/");
|
||||||
const IS_TEST_RUNNER = window._lightpanda_skip_auto_assert === true;
|
|
||||||
|
|
||||||
window.testing = {
|
window.testing = {
|
||||||
fail: fail,
|
fail: fail,
|
||||||
@@ -114,17 +113,17 @@
|
|||||||
eventually: eventually,
|
eventually: eventually,
|
||||||
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
IS_TEST_RUNNER: IS_TEST_RUNNER,
|
||||||
HOST: '127.0.0.1',
|
HOST: '127.0.0.1',
|
||||||
ORIGIN: 'http://127.0.0.1:9582/',
|
ORIGIN: 'http://127.0.0.1:9582',
|
||||||
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
|
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.navigator.userAgent.startsWith("Lightpanda/") == false) {
|
if (IS_TEST_RUNNER === false) {
|
||||||
// The page is running in a different browser. Probably a developer making sure
|
// The page is running in a different browser. Probably a developer making sure
|
||||||
// a test is correct. There are a few tweaks we need to do to make this a
|
// a test is correct. There are a few tweaks we need to do to make this a
|
||||||
// seemless, namely around adapting paths/urls.
|
// seemless, namely around adapting paths/urls.
|
||||||
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
|
console.warn(`The page is not being executed in the test runner, certain behavior has been adjusted`);
|
||||||
window.testing.HOST = location.hostname;
|
window.testing.HOST = location.hostname;
|
||||||
window.testing.ORIGIN = location.origin + '/';
|
window.testing.ORIGIN = location.origin;
|
||||||
window.testing.BASE_URL = location.origin + '/src/browser/tests/';
|
window.testing.BASE_URL = location.origin + '/src/browser/tests/';
|
||||||
window.addEventListener('load', testing.assertOk);
|
window.addEventListener('load', testing.assertOk);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<body onload="loaded()"></body>
|
<body onload="loadEvent = event"></body>
|
||||||
<script src="../testing.js"></script>
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
<script id=bodyOnLoad2>
|
<script id=bodyOnLoad2>
|
||||||
let called = 0;
|
// Per spec, the handler is compiled as: function(event) { loadEvent = event }
|
||||||
function loaded(e) {
|
// Verify: handler fires, "event" parameter is a proper Event, and handler is a function.
|
||||||
called += 1;
|
let loadEvent = null;
|
||||||
}
|
|
||||||
|
|
||||||
testing.eventually(() => {
|
testing.eventually(() => {
|
||||||
testing.expectEqual(1, called);
|
testing.expectEqual("function", typeof document.body.onload);
|
||||||
|
testing.expectTrue(loadEvent instanceof Event);
|
||||||
|
testing.expectEqual("load", loadEvent.type);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
28
src/browser/tests/window/body_onload3.html
Normal file
28
src/browser/tests/window/body_onload3.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<body onload="called++"></body>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=bodyOnLoad3>
|
||||||
|
// Per spec, the handler is compiled as: function(event) { called++ }
|
||||||
|
// Verify: handler fires exactly once, and body.onload reflects to window.onload.
|
||||||
|
let called = 0;
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
// The attribute handler should have fired exactly once.
|
||||||
|
testing.expectEqual(1, called);
|
||||||
|
|
||||||
|
// body.onload is a Window-reflecting handler per spec.
|
||||||
|
testing.expectEqual("function", typeof document.body.onload);
|
||||||
|
testing.expectEqual(document.body.onload, window.onload);
|
||||||
|
|
||||||
|
// Setting body.onload via property replaces the attribute handler.
|
||||||
|
let propertyCalled = false;
|
||||||
|
document.body.onload = function() { propertyCalled = true; };
|
||||||
|
testing.expectEqual(document.body.onload, window.onload);
|
||||||
|
|
||||||
|
// Setting onload to null removes the handler.
|
||||||
|
document.body.onload = null;
|
||||||
|
testing.expectEqual(null, document.body.onload);
|
||||||
|
testing.expectEqual(null, window.onload);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
testing.expectEqual('ceil', atob('Y2VpbA')); // 6 chars, len%4==2, needs '=='
|
||||||
|
|
||||||
// length % 4 == 1 must still throw
|
// length % 4 == 1 must still throw
|
||||||
testing.expectError('InvalidCharacterError: Invalid Character', () => {
|
testing.expectError('InvalidCharacterError', () => {
|
||||||
atob('Y');
|
atob('Y');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
34
src/browser/tests/window/window_event.html
Normal file
34
src/browser/tests/window/window_event.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id=windowEventUndefinedOutsideHandler>
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=windowEventSetDuringWindowHandler>
|
||||||
|
var capturedEvent = null;
|
||||||
|
|
||||||
|
window.addEventListener('test-event', function(e) {
|
||||||
|
capturedEvent = window.event;
|
||||||
|
});
|
||||||
|
|
||||||
|
var ev = new Event('test-event');
|
||||||
|
window.dispatchEvent(ev);
|
||||||
|
|
||||||
|
testing.expectEqual(ev, capturedEvent);
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=windowEventRestoredAfterHandler>
|
||||||
|
var captured2 = null;
|
||||||
|
|
||||||
|
window.addEventListener('test-event-2', function(e) {
|
||||||
|
captured2 = window.event;
|
||||||
|
});
|
||||||
|
|
||||||
|
var ev2 = new Event('test-event-2');
|
||||||
|
window.dispatchEvent(ev2);
|
||||||
|
|
||||||
|
testing.expectEqual(ev2, captured2);
|
||||||
|
testing.expectEqual(undefined, window.event);
|
||||||
|
</script>
|
||||||
@@ -125,8 +125,8 @@ pub fn whenDefined(self: *CustomElementRegistry, name: []const u8, page: *Page)
|
|||||||
return local.resolvePromise(definition.constructor);
|
return local.resolvePromise(definition.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateName(name) catch |err| {
|
validateName(name) catch |err| switch (err) {
|
||||||
return local.rejectPromise(DOMException.fromError(err) orelse unreachable);
|
error.SyntaxError => return local.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const gop = try self._when_defined.getOrPut(page.arena, name);
|
const gop = try self._when_defined.getOrPut(page.arena, name);
|
||||||
|
|||||||
@@ -104,13 +104,27 @@ pub fn getMessage(self: *const DOMException) []const u8 {
|
|||||||
}
|
}
|
||||||
return switch (self._code) {
|
return switch (self._code) {
|
||||||
.none => "",
|
.none => "",
|
||||||
.invalid_character_error => "Invalid Character",
|
|
||||||
.index_size_error => "Index or size is negative or greater than the allowed amount",
|
.index_size_error => "Index or size is negative or greater than the allowed amount",
|
||||||
.syntax_error => "Syntax Error",
|
.hierarchy_error => "The operation would yield an incorrect node tree",
|
||||||
.not_supported => "Not Supported",
|
.wrong_document_error => "The object is in the wrong document",
|
||||||
.not_found => "Not Found",
|
.invalid_character_error => "The string contains invalid characters",
|
||||||
.hierarchy_error => "Hierarchy Error",
|
.no_modification_allowed_error => "The object can not be modified",
|
||||||
else => @tagName(self._code),
|
.not_found => "The object can not be found here",
|
||||||
|
.not_supported => "The operation is not supported",
|
||||||
|
.inuse_attribute_error => "The attribute already in use",
|
||||||
|
.invalid_state_error => "The object is in an invalid state",
|
||||||
|
.syntax_error => "The string did not match the expected pattern",
|
||||||
|
.invalid_modification_error => "The object can not be modified in this way",
|
||||||
|
.namespace_error => "The operation is not allowed by Namespaces in XML",
|
||||||
|
.invalid_access_error => "The object does not support the operation or argument",
|
||||||
|
.security_error => "The operation is insecure",
|
||||||
|
.network_error => "A network error occurred",
|
||||||
|
.abort_error => "The operation was aborted",
|
||||||
|
.url_mismatch_error => "The given URL does not match another URL",
|
||||||
|
.quota_exceeded_error => "The quota has been exceeded",
|
||||||
|
.timeout_error => "The operation timed out",
|
||||||
|
.invalid_node_type_error => "The supplied node is incorrect or has an incorrect ancestor for this operation",
|
||||||
|
.data_clone_error => "The object can not be cloned",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,15 +86,15 @@ pub fn parseFromString(
|
|||||||
var parser = Parser.init(arena, doc_node, page);
|
var parser = Parser.init(arena, doc_node, page);
|
||||||
parser.parseXML(html);
|
parser.parseXML(html);
|
||||||
|
|
||||||
if (parser.err) |pe| {
|
if (parser.err != null or doc_node.firstChild() == null) {
|
||||||
return pe.err;
|
// Return a document with a <parsererror> element per spec.
|
||||||
|
const err_doc = try page._factory.document(XMLDocument{ ._proto = undefined });
|
||||||
|
var err_parser = Parser.init(arena, err_doc.asNode(), page);
|
||||||
|
err_parser.parseXML("<parsererror xmlns=\"http://www.mozilla.org/newlayout/xml/parsererror.xml\">error</parsererror>");
|
||||||
|
return err_doc.asDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
const first_child = doc_node.firstChild() orelse {
|
const first_child = doc_node.firstChild().?;
|
||||||
// Empty XML or no root element - this is a parse error.
|
|
||||||
// TODO: Return a document with a <parsererror> element per spec.
|
|
||||||
return error.JsException;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If first node is a `ProcessingInstruction`, skip it.
|
// If first node is a `ProcessingInstruction`, skip it.
|
||||||
if (first_child.getNodeType() == 7) {
|
if (first_child.getNodeType() == 7) {
|
||||||
|
|||||||
@@ -365,6 +365,11 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i
|
|||||||
return (try KeyboardEvent.init("", null, page)).asEvent();
|
return (try KeyboardEvent.init("", null, page)).asEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, normalized, "inputevent")) {
|
||||||
|
const InputEvent = @import("event/InputEvent.zig");
|
||||||
|
return (try InputEvent.init("", null, page)).asEvent();
|
||||||
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
|
if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) {
|
||||||
const MouseEvent = @import("event/MouseEvent.zig");
|
const MouseEvent = @import("event/MouseEvent.zig");
|
||||||
return (try MouseEvent.init("", null, page)).asEvent();
|
return (try MouseEvent.init("", null, page)).asEvent();
|
||||||
|
|||||||
@@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
|
||||||
self._callback.release();
|
if (shutdown) {
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
self._callback.release();
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
session.releaseArena(self._arena);
|
||||||
|
} else if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.releaseArena(self._arena);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
|
||||||
@@ -111,7 +111,6 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
page.js.strongRef(self);
|
|
||||||
try page.registerIntersectionObserver(self);
|
try page.registerIntersectionObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,22 +145,18 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self._observing.items.len == 0) {
|
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
|
||||||
page.unregisterIntersectionObserver(self);
|
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
self._previous_states.clearRetainingCapacity();
|
self._previous_states.clearRetainingCapacity();
|
||||||
|
|
||||||
for (self._pending_entries.items) |entry| {
|
for (self._pending_entries.items) |entry| {
|
||||||
entry.deinit(false, page._session);
|
entry.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_entries.clearRetainingCapacity();
|
self._pending_entries.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
page.unregisterIntersectionObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
|
||||||
@@ -363,7 +358,6 @@ pub const JsApi = struct {
|
|||||||
pub const name = "IntersectionObserver";
|
pub const name = "IntersectionObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const weak = true;
|
|
||||||
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
|
||||||
self._callback.release();
|
if (shutdown) {
|
||||||
if ((comptime IS_DEBUG) and !shutdown) {
|
self._callback.release();
|
||||||
std.debug.assert(self._observing.items.len == 0);
|
session.releaseArena(self._arena);
|
||||||
|
} else if (comptime IS_DEBUG) {
|
||||||
|
std.debug.assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.releaseArena(self._arena);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
|
||||||
@@ -158,7 +158,6 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
|
|
||||||
// Register with page if this is our first observation
|
// Register with page if this is our first observation
|
||||||
if (self._observing.items.len == 0) {
|
if (self._observing.items.len == 0) {
|
||||||
page.js.strongRef(self);
|
|
||||||
try page.registerMutationObserver(self);
|
try page.registerMutationObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +168,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
pub fn disconnect(self: *MutationObserver, page: *Page) void {
|
||||||
page.unregisterMutationObserver(self);
|
|
||||||
self._observing.clearRetainingCapacity();
|
|
||||||
for (self._pending_records.items) |record| {
|
for (self._pending_records.items) |record| {
|
||||||
record.deinit(false, page._session);
|
record.deinit(false, page._session);
|
||||||
}
|
}
|
||||||
self._pending_records.clearRetainingCapacity();
|
self._pending_records.clearRetainingCapacity();
|
||||||
page.js.safeWeakRef(self);
|
|
||||||
|
self._observing.clearRetainingCapacity();
|
||||||
|
page.unregisterMutationObserver(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
|
||||||
@@ -441,7 +440,6 @@ pub const JsApi = struct {
|
|||||||
pub const name = "MutationObserver";
|
pub const name = "MutationObserver";
|
||||||
pub const prototype_chain = bridge.prototypeChain();
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
pub const weak = true;
|
|
||||||
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,21 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
const js = @import("../js/js.zig");
|
const js = @import("../js/js.zig");
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
const PluginArray = @import("PluginArray.zig");
|
const PluginArray = @import("PluginArray.zig");
|
||||||
|
const Permissions = @import("Permissions.zig");
|
||||||
|
const StorageManager = @import("StorageManager.zig");
|
||||||
|
|
||||||
const Navigator = @This();
|
const Navigator = @This();
|
||||||
_pad: bool = false,
|
_pad: bool = false,
|
||||||
_plugins: PluginArray = .{},
|
_plugins: PluginArray = .{},
|
||||||
|
_permissions: Permissions = .{},
|
||||||
|
_storage: StorageManager = .{},
|
||||||
|
|
||||||
pub const init: Navigator = .{};
|
pub const init: Navigator = .{};
|
||||||
|
|
||||||
@@ -55,6 +63,19 @@ pub fn getPlugins(self: *Navigator) *PluginArray {
|
|||||||
return &self._plugins;
|
return &self._plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getPermissions(self: *Navigator) *Permissions {
|
||||||
|
return &self._permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getStorage(self: *Navigator) *StorageManager {
|
||||||
|
return &self._storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise {
|
||||||
|
log.info(.not_implemented, "navigator.getBattery", .{});
|
||||||
|
return page.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void {
|
||||||
try validateProtocolHandlerScheme(scheme);
|
try validateProtocolHandlerScheme(scheme);
|
||||||
try validateProtocolHandlerURL(url, page);
|
try validateProtocolHandlerURL(url, page);
|
||||||
@@ -144,6 +165,7 @@ pub const JsApi = struct {
|
|||||||
pub const onLine = bridge.property(true, .{ .template = false });
|
pub const onLine = bridge.property(true, .{ .template = false });
|
||||||
pub const cookieEnabled = bridge.property(true, .{ .template = false });
|
pub const cookieEnabled = bridge.property(true, .{ .template = false });
|
||||||
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
|
pub const hardwareConcurrency = bridge.property(4, .{ .template = false });
|
||||||
|
pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false });
|
||||||
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
|
pub const maxTouchPoints = bridge.property(0, .{ .template = false });
|
||||||
pub const vendor = bridge.property("", .{ .template = false });
|
pub const vendor = bridge.property("", .{ .template = false });
|
||||||
pub const product = bridge.property("Gecko", .{ .template = false });
|
pub const product = bridge.property("Gecko", .{ .template = false });
|
||||||
@@ -156,4 +178,12 @@ pub const JsApi = struct {
|
|||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
|
pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{});
|
||||||
|
pub const getBattery = bridge.function(Navigator.getBattery, .{});
|
||||||
|
pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});
|
||||||
|
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testing = @import("../../testing.zig");
|
||||||
|
test "WebApi: Navigator" {
|
||||||
|
try testing.htmlRunner("navigator", .{});
|
||||||
|
}
|
||||||
|
|||||||
94
src/browser/webapi/Permissions.zig
Normal file
94
src/browser/webapi/Permissions.zig
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
const Session = @import("../Session.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{ Permissions, PermissionStatus };
|
||||||
|
}
|
||||||
|
|
||||||
|
const Permissions = @This();
|
||||||
|
|
||||||
|
// Padding to avoid zero-size struct pointer collisions
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
const QueryDescriptor = struct {
|
||||||
|
name: []const u8,
|
||||||
|
};
|
||||||
|
// We always report 'prompt' (the default safe value — neither granted nor denied).
|
||||||
|
pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promise {
|
||||||
|
const arena = try page.getArena(.{ .debug = "PermissionStatus" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
|
||||||
|
const status = try arena.create(PermissionStatus);
|
||||||
|
status.* = .{
|
||||||
|
._arena = arena,
|
||||||
|
._state = "prompt",
|
||||||
|
._name = try arena.dupe(u8, qd.name),
|
||||||
|
};
|
||||||
|
return page.js.local.?.resolvePromise(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionStatus = struct {
|
||||||
|
_arena: Allocator,
|
||||||
|
_name: []const u8,
|
||||||
|
_state: []const u8,
|
||||||
|
|
||||||
|
pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {
|
||||||
|
session.releaseArena(self._arena);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getName(self: *const PermissionStatus) []const u8 {
|
||||||
|
return self._name;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getState(self: *const PermissionStatus) []const u8 {
|
||||||
|
return self._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(PermissionStatus);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "PermissionStatus";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
|
||||||
|
};
|
||||||
|
pub const name = bridge.accessor(getName, null, .{});
|
||||||
|
pub const state = bridge.accessor(getState, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(Permissions);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "Permissions";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const query = bridge.function(Permissions.query, .{ .dom_exception = true });
|
||||||
|
};
|
||||||
@@ -40,6 +40,7 @@ _mode: Mode,
|
|||||||
_host: *Element,
|
_host: *Element,
|
||||||
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
_elements_by_id: std.StringHashMapUnmanaged(*Element) = .{},
|
||||||
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
_removed_ids: std.StringHashMapUnmanaged(void) = .{},
|
||||||
|
_adopted_style_sheets: ?js.Object.Global = null,
|
||||||
|
|
||||||
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
pub fn init(host: *Element, mode: Mode, page: *Page) !*ShadowRoot {
|
||||||
return page._factory.documentFragment(ShadowRoot{
|
return page._factory.documentFragment(ShadowRoot{
|
||||||
@@ -99,6 +100,20 @@ pub fn getElementById(self: *ShadowRoot, id: []const u8, page: *Page) ?*Element
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getAdoptedStyleSheets(self: *ShadowRoot, page: *Page) !js.Object.Global {
|
||||||
|
if (self._adopted_style_sheets) |ass| {
|
||||||
|
return ass;
|
||||||
|
}
|
||||||
|
const js_arr = page.js.local.?.newArray(0);
|
||||||
|
const js_obj = js_arr.toObject();
|
||||||
|
self._adopted_style_sheets = try js_obj.persist();
|
||||||
|
return self._adopted_style_sheets.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setAdoptedStyleSheets(self: *ShadowRoot, sheets: js.Object) !void {
|
||||||
|
self._adopted_style_sheets = try sheets.persist();
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(ShadowRoot);
|
pub const bridge = js.Bridge(ShadowRoot);
|
||||||
|
|
||||||
@@ -121,6 +136,7 @@ pub const JsApi = struct {
|
|||||||
}
|
}
|
||||||
return self.getElementById(try value.toZig([]const u8), page);
|
return self.getElementById(try value.toZig([]const u8), page);
|
||||||
}
|
}
|
||||||
|
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../testing.zig");
|
const testing = @import("../../testing.zig");
|
||||||
|
|||||||
71
src/browser/webapi/StorageManager.zig
Normal file
71
src/browser/webapi/StorageManager.zig
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const js = @import("../js/js.zig");
|
||||||
|
const Page = @import("../Page.zig");
|
||||||
|
|
||||||
|
pub fn registerTypes() []const type {
|
||||||
|
return &.{ StorageManager, StorageEstimate };
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageManager = @This();
|
||||||
|
|
||||||
|
_pad: bool = false,
|
||||||
|
|
||||||
|
pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise {
|
||||||
|
const est = try page._factory.create(StorageEstimate{
|
||||||
|
._usage = 0,
|
||||||
|
._quota = 1024 * 1024 * 1024, // 1 GiB
|
||||||
|
});
|
||||||
|
return page.js.local.?.resolvePromise(est);
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageEstimate = struct {
|
||||||
|
_quota: u64,
|
||||||
|
_usage: u64,
|
||||||
|
|
||||||
|
fn getUsage(self: *const StorageEstimate) u64 {
|
||||||
|
return self._usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getQuota(self: *const StorageEstimate) u64 {
|
||||||
|
return self._quota;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(StorageEstimate);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "StorageEstimate";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
};
|
||||||
|
pub const quota = bridge.accessor(getQuota, null, .{});
|
||||||
|
pub const usage = bridge.accessor(getUsage, null, .{});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(StorageManager);
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "StorageManager";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const empty_with_no_proto = true;
|
||||||
|
};
|
||||||
|
pub const estimate = bridge.function(StorageManager.estimate, .{});
|
||||||
|
};
|
||||||
@@ -96,8 +96,8 @@ pub fn generateKey(
|
|||||||
key_usages: []const []const u8,
|
key_usages: []const []const u8,
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch |err| {
|
const key_or_pair = CryptoKey.init(algorithm, extractable, key_usages, page) catch {
|
||||||
return page.js.local.?.rejectPromise(@errorName(err));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.SyntaxError } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(key_or_pair);
|
return page.js.local.?.resolvePromise(key_or_pair);
|
||||||
@@ -112,7 +112,7 @@ pub fn exportKey(
|
|||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!key.canExportKey()) {
|
if (!key.canExportKey()) {
|
||||||
return error.InvalidAccessError;
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std.mem.eql(u8, format, "raw")) {
|
if (std.mem.eql(u8, format, "raw")) {
|
||||||
@@ -124,9 +124,10 @@ pub fn exportKey(
|
|||||||
|
|
||||||
if (is_unsupported) {
|
if (is_unsupported) {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
|
log.warn(.not_implemented, "SubtleCrypto.exportKey", .{ .format = format });
|
||||||
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
|
return page.js.local.?.rejectPromise(.{ .type_error = "invalid format" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a secret key from a master key.
|
/// Derive a secret key from a master key.
|
||||||
@@ -148,7 +149,7 @@ pub fn deriveBits(
|
|||||||
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
|
log.warn(.not_implemented, "SubtleCrypto.deriveBits", .{ .name = name });
|
||||||
}
|
}
|
||||||
|
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.NotSupported));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -185,19 +186,19 @@ pub fn sign(
|
|||||||
.hmac => {
|
.hmac => {
|
||||||
// Verify algorithm.
|
// Verify algorithm.
|
||||||
if (!algorithm.isHMAC()) {
|
if (!algorithm.isHMAC()) {
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call sign for HMAC.
|
// Call sign for HMAC.
|
||||||
const result = key.signHMAC(data, page) catch |err| {
|
const result = key.signHMAC(data, page) catch {
|
||||||
return page.js.local.?.rejectPromise(@errorName(err));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return page.js.local.?.resolvePromise(result);
|
return page.js.local.?.resolvePromise(result);
|
||||||
},
|
},
|
||||||
else => {
|
else => {
|
||||||
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
log.warn(.not_implemented, "SubtleCrypto.sign", .{ .key_type = key._type });
|
||||||
return page.js.local.?.rejectPromise(@errorName(error.InvalidAccessError));
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -211,18 +212,20 @@ pub fn verify(
|
|||||||
data: []const u8, // ArrayBuffer.
|
data: []const u8, // ArrayBuffer.
|
||||||
page: *Page,
|
page: *Page,
|
||||||
) !js.Promise {
|
) !js.Promise {
|
||||||
if (!algorithm.isHMAC()) return error.InvalidAccessError;
|
if (!algorithm.isHMAC()) {
|
||||||
|
return page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } });
|
||||||
|
}
|
||||||
|
|
||||||
return switch (key._type) {
|
return switch (key._type) {
|
||||||
.hmac => key.verifyHMAC(signature, data, page),
|
.hmac => key.verifyHMAC(signature, data, page),
|
||||||
else => return error.InvalidAccessError,
|
else => page.js.local.?.rejectPromise(.{ .dom_exception = .{ .err = error.InvalidAccessError } }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray(u8), page: *Page) !js.Promise {
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
if (algorithm.len > 10) {
|
if (algorithm.len > 10) {
|
||||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
const normalized = std.ascii.lowerString(&page.buf, algorithm);
|
||||||
if (std.mem.eql(u8, normalized, "sha-1")) {
|
if (std.mem.eql(u8, normalized, "sha-1")) {
|
||||||
@@ -245,7 +248,7 @@ pub fn digest(_: *const SubtleCrypto, algorithm: []const u8, data: js.TypedArray
|
|||||||
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
Sha512.hash(data.values, page.buf[0..Sha512.digest_length], .{});
|
||||||
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
return local.resolvePromise(js.ArrayBuffer{ .values = page.buf[0..Sha512.digest_length] });
|
||||||
}
|
}
|
||||||
return local.rejectPromise(DOMException.fromError(error.NotSupported));
|
return local.rejectPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the desired digest by its name.
|
/// Returns the desired digest by its name.
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ _on_load: ?js.Function.Global = null,
|
|||||||
_on_pageshow: ?js.Function.Global = null,
|
_on_pageshow: ?js.Function.Global = null,
|
||||||
_on_popstate: ?js.Function.Global = null,
|
_on_popstate: ?js.Function.Global = null,
|
||||||
_on_error: ?js.Function.Global = null,
|
_on_error: ?js.Function.Global = null,
|
||||||
_on_unhandled_rejection: ?js.Function.Global = null, // TODO: invoke on error
|
_on_message: ?js.Function.Global = null,
|
||||||
|
_on_rejection_handled: ?js.Function.Global = null,
|
||||||
|
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||||
|
_current_event: ?*Event = null,
|
||||||
_location: *Location,
|
_location: *Location,
|
||||||
_timer_id: u30 = 0,
|
_timer_id: u30 = 0,
|
||||||
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
|
||||||
@@ -89,6 +92,10 @@ pub fn asEventTarget(self: *Window) *EventTarget {
|
|||||||
return self._proto;
|
return self._proto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getEvent(self: *const Window) ?*Event {
|
||||||
|
return self._current_event;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getSelf(self: *Window) *Window {
|
pub fn getSelf(self: *Window) *Window {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -208,6 +215,22 @@ pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
|
|||||||
self._on_error = getFunctionFromSetter(setter);
|
self._on_error = getFunctionFromSetter(setter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getOnMessage(self: *const Window) ?js.Function.Global {
|
||||||
|
return self._on_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnMessage(self: *Window, setter: ?FunctionSetter) void {
|
||||||
|
self._on_message = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getOnRejectionHandled(self: *const Window) ?js.Function.Global {
|
||||||
|
return self._on_rejection_handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setOnRejectionHandled(self: *Window, setter: ?FunctionSetter) void {
|
||||||
|
self._on_rejection_handled = getFunctionFromSetter(setter);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function.Global {
|
||||||
return self._on_unhandled_rejection;
|
return self._on_unhandled_rejection;
|
||||||
}
|
}
|
||||||
@@ -334,7 +357,11 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
|
|||||||
|
|
||||||
const event = error_event.asEvent();
|
const event = error_event.asEvent();
|
||||||
event._prevent_default = prevent_default;
|
event._prevent_default = prevent_default;
|
||||||
try page._event_manager.dispatch(self.asEventTarget(), event);
|
// Pass null as handler: onerror was already called above with 5 args.
|
||||||
|
// We still dispatch so that addEventListener('error', ...) listeners fire.
|
||||||
|
try page._event_manager.dispatchDirect(self.asEventTarget(), event, null, .{
|
||||||
|
.context = "window.reportError",
|
||||||
|
});
|
||||||
|
|
||||||
if (comptime builtin.is_test == false) {
|
if (comptime builtin.is_test == false) {
|
||||||
if (!event._prevent_default) {
|
if (!event._prevent_default) {
|
||||||
@@ -369,19 +396,26 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
|
|||||||
// In a full implementation, we would validate the origin
|
// In a full implementation, we would validate the origin
|
||||||
_ = target_origin;
|
_ = target_origin;
|
||||||
|
|
||||||
// postMessage queues a task (not a microtask), so use the scheduler
|
// self = the window that will get the message
|
||||||
const arena = try page.getArena(.{ .debug = "Window.schedule" });
|
// page = the context calling postMessage
|
||||||
errdefer page.releaseArena(arena);
|
const target_page = self._page;
|
||||||
|
const source_window = target_page.js.getIncumbent().window;
|
||||||
|
|
||||||
const origin = try self._location.getOrigin(page);
|
const arena = try target_page.getArena(.{ .debug = "Window.postMessage" });
|
||||||
|
errdefer target_page.releaseArena(arena);
|
||||||
|
|
||||||
|
// Origin should be the source window's origin (where the message came from)
|
||||||
|
const origin = try source_window._location.getOrigin(page);
|
||||||
const callback = try arena.create(PostMessageCallback);
|
const callback = try arena.create(PostMessageCallback);
|
||||||
callback.* = .{
|
callback.* = .{
|
||||||
.page = page,
|
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.message = message,
|
.message = message,
|
||||||
|
.page = target_page,
|
||||||
|
.source = source_window,
|
||||||
.origin = try arena.dupe(u8, origin),
|
.origin = try arena.dupe(u8, origin),
|
||||||
};
|
};
|
||||||
try page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
|
||||||
|
try target_page.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||||
.name = "postMessage",
|
.name = "postMessage",
|
||||||
.low_priority = false,
|
.low_priority = false,
|
||||||
.finalizer = PostMessageCallback.cancelled,
|
.finalizer = PostMessageCallback.cancelled,
|
||||||
@@ -547,7 +581,7 @@ pub fn scrollBy(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
|
|||||||
return self.scrollTo(.{ .x = absx }, absy, page);
|
return self.scrollTo(.{ .x = absx }, absy, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
|
pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.PromiseRejection, page: *Page) !void {
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.js, "unhandled rejection", .{
|
log.debug(.js, "unhandled rejection", .{
|
||||||
.value = rejection.reason(),
|
.value = rejection.reason(),
|
||||||
@@ -555,13 +589,20 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event_name, const attribute_callback = blk: {
|
||||||
|
if (no_handler) {
|
||||||
|
break :blk .{ "unhandledrejection", self._on_unhandled_rejection };
|
||||||
|
}
|
||||||
|
break :blk .{ "rejectionhandled", self._on_rejection_handled };
|
||||||
|
};
|
||||||
|
|
||||||
const target = self.asEventTarget();
|
const target = self.asEventTarget();
|
||||||
if (page._event_manager.hasDirectListeners(target, "unhandledrejection", self._on_unhandled_rejection)) {
|
if (page._event_manager.hasDirectListeners(target, event_name, attribute_callback)) {
|
||||||
const event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
|
const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{
|
||||||
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
.reason = if (rejection.reason()) |r| try r.temp() else null,
|
||||||
.promise = try rejection.promise().temp(),
|
.promise = try rejection.promise().temp(),
|
||||||
}, page)).asEvent();
|
}, page)).asEvent();
|
||||||
try page._event_manager.dispatchDirect(target, event, self._on_unhandled_rejection, .{ .context = "window.unhandledrejection" });
|
try page._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,6 +743,7 @@ const ScheduleCallback = struct {
|
|||||||
|
|
||||||
const PostMessageCallback = struct {
|
const PostMessageCallback = struct {
|
||||||
page: *Page,
|
page: *Page,
|
||||||
|
source: *Window,
|
||||||
arena: Allocator,
|
arena: Allocator,
|
||||||
origin: []const u8,
|
origin: []const u8,
|
||||||
message: js.Value.Temp,
|
message: js.Value.Temp,
|
||||||
@@ -712,7 +754,7 @@ const PostMessageCallback = struct {
|
|||||||
|
|
||||||
fn cancelled(ctx: *anyopaque) void {
|
fn cancelled(ctx: *anyopaque) void {
|
||||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||||
self.page.releaseArena(self.arena);
|
self.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(ctx: *anyopaque) !?u32 {
|
fn run(ctx: *anyopaque) !?u32 {
|
||||||
@@ -722,14 +764,17 @@ const PostMessageCallback = struct {
|
|||||||
const page = self.page;
|
const page = self.page;
|
||||||
const window = page.window;
|
const window = page.window;
|
||||||
|
|
||||||
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
const event_target = window.asEventTarget();
|
||||||
.data = self.message,
|
if (page._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
|
||||||
.origin = self.origin,
|
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||||
.source = window,
|
.data = self.message,
|
||||||
.bubbles = false,
|
.origin = self.origin,
|
||||||
.cancelable = false,
|
.source = self.source,
|
||||||
}, page)).asEvent();
|
.bubbles = false,
|
||||||
try page._event_manager.dispatch(window.asEventTarget(), event);
|
.cancelable = false,
|
||||||
|
}, page)).asEvent();
|
||||||
|
try page._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -783,7 +828,10 @@ pub const JsApi = struct {
|
|||||||
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
||||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||||
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
||||||
|
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
|
||||||
|
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
|
||||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||||
|
pub const event = bridge.accessor(Window.getEvent, null, .{ .null_as_undefined = true });
|
||||||
pub const fetch = bridge.function(Window.fetch, .{});
|
pub const fetch = bridge.function(Window.fetch, .{});
|
||||||
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
|
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
|
||||||
pub const setTimeout = bridge.function(Window.setTimeout, .{});
|
pub const setTimeout = bridge.function(Window.setTimeout, .{});
|
||||||
@@ -853,3 +901,7 @@ test "WebApi: Window" {
|
|||||||
test "WebApi: Window scroll" {
|
test "WebApi: Window scroll" {
|
||||||
try testing.htmlRunner("window_scroll.html", .{});
|
try testing.htmlRunner("window_scroll.html", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "WebApi: Window.onerror" {
|
||||||
|
try testing.htmlRunner("event/report_error.html", .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,6 +117,47 @@ pub fn submit(self: *Form, page: *Page) !void {
|
|||||||
return page.submitForm(null, self, .{ .fire_event = false });
|
return page.submitForm(null, self, .{ .fire_event = false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit
|
||||||
|
/// Like submit(), but fires the submit event and validates the form.
|
||||||
|
pub fn requestSubmit(self: *Form, submitter: ?*Element, page: *Page) !void {
|
||||||
|
const submitter_element = if (submitter) |s| blk: {
|
||||||
|
// The submitter must be a submit button.
|
||||||
|
if (!isSubmitButton(s)) return error.TypeError;
|
||||||
|
|
||||||
|
// The submitter's form owner must be this form element.
|
||||||
|
const submitter_form = getFormOwner(s, page);
|
||||||
|
if (submitter_form == null or submitter_form.? != self) return error.NotFound;
|
||||||
|
|
||||||
|
break :blk s;
|
||||||
|
} else self.asElement();
|
||||||
|
|
||||||
|
return page.submitForm(submitter_element, self, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the element is a submit button per the HTML spec:
|
||||||
|
/// - <input type="submit"> or <input type="image">
|
||||||
|
/// - <button type="submit"> (including default, since button's default type is "submit")
|
||||||
|
fn isSubmitButton(element: *Element) bool {
|
||||||
|
if (element.is(Input)) |input| {
|
||||||
|
return input._input_type == .submit or input._input_type == .image;
|
||||||
|
}
|
||||||
|
if (element.is(Button)) |button| {
|
||||||
|
return std.mem.eql(u8, button.getType(), "submit");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the form owner of a submittable element (Input or Button).
|
||||||
|
fn getFormOwner(element: *Element, page: *Page) ?*Form {
|
||||||
|
if (element.is(Input)) |input| {
|
||||||
|
return input.getForm(page);
|
||||||
|
}
|
||||||
|
if (element.is(Button)) |button| {
|
||||||
|
return button.getForm(page);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Form);
|
pub const bridge = js.Bridge(Form);
|
||||||
pub const Meta = struct {
|
pub const Meta = struct {
|
||||||
@@ -132,6 +173,7 @@ pub const JsApi = struct {
|
|||||||
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
||||||
pub const length = bridge.accessor(Form.getLength, null, .{});
|
pub const length = bridge.accessor(Form.getLength, null, .{});
|
||||||
pub const submit = bridge.function(Form.submit, .{});
|
pub const submit = bridge.function(Form.submit, .{});
|
||||||
|
pub const requestSubmit = bridge.function(Form.requestSubmit, .{ .dom_exception = true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const testing = @import("../../../../testing.zig");
|
const testing = @import("../../../../testing.zig");
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const HtmlElement = @import("../Html.zig");
|
|||||||
const Form = @import("Form.zig");
|
const Form = @import("Form.zig");
|
||||||
const Selection = @import("../../Selection.zig");
|
const Selection = @import("../../Selection.zig");
|
||||||
const Event = @import("../../Event.zig");
|
const Event = @import("../../Event.zig");
|
||||||
|
const InputEvent = @import("../../event/InputEvent.zig");
|
||||||
|
|
||||||
const Input = @This();
|
const Input = @This();
|
||||||
|
|
||||||
@@ -103,6 +104,11 @@ fn dispatchSelectionChangeEvent(self: *Input, page: *Page) !void {
|
|||||||
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, page: *Page) !void {
|
||||||
|
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, page);
|
||||||
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn asElement(self: *Input) *Element {
|
pub fn asElement(self: *Input) *Element {
|
||||||
return self._proto._proto;
|
return self._proto._proto;
|
||||||
}
|
}
|
||||||
@@ -425,6 +431,7 @@ pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
|
|||||||
try self.setValue(new_value, page);
|
try self.setValue(new_value, page);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
try self.dispatchInputEvent(str, "insertText", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSelectionDirection(self: *const Input) []const u8 {
|
pub fn getSelectionDirection(self: *const Input) []const u8 {
|
||||||
|
|||||||
@@ -93,13 +93,21 @@ pub fn linkAddedCallback(self: *Link, page: *Page) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = self.asElement();
|
const element = self.asElement();
|
||||||
// Exit if rel not set.
|
|
||||||
const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return;
|
||||||
// Exit if rel is not stylesheet.
|
const loadable_rels = std.StaticStringMap(void).initComptime(.{
|
||||||
if (!std.mem.eql(u8, rel, "stylesheet")) return;
|
.{ "stylesheet", {} },
|
||||||
// Exit if href not set.
|
.{ "preload", {} },
|
||||||
|
.{ "modulepreload", {} },
|
||||||
|
});
|
||||||
|
if (loadable_rels.has(rel) == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
const href = element.getAttributeSafe(comptime .wrap("href")) orelse return;
|
||||||
if (href.len == 0) return;
|
if (href.len == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try page._to_load.append(page.arena, self._proto);
|
try page._to_load.append(page.arena, self._proto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const HtmlElement = @import("../Html.zig");
|
|||||||
const Form = @import("Form.zig");
|
const Form = @import("Form.zig");
|
||||||
const Selection = @import("../../Selection.zig");
|
const Selection = @import("../../Selection.zig");
|
||||||
const Event = @import("../../Event.zig");
|
const Event = @import("../../Event.zig");
|
||||||
|
const InputEvent = @import("../../event/InputEvent.zig");
|
||||||
|
|
||||||
const TextArea = @This();
|
const TextArea = @This();
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ fn dispatchSelectionChangeEvent(self: *TextArea, page: *Page) !void {
|
|||||||
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dispatchInputEvent(self: *TextArea, data: ?[]const u8, input_type: []const u8, page: *Page) !void {
|
||||||
|
const event = try InputEvent.initTrusted(comptime .wrap("input"), .{ .data = data, .inputType = input_type }, page);
|
||||||
|
try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn asElement(self: *TextArea) *Element {
|
pub fn asElement(self: *TextArea) *Element {
|
||||||
return self._proto._proto;
|
return self._proto._proto;
|
||||||
}
|
}
|
||||||
@@ -189,6 +195,7 @@ pub fn innerInsert(self: *TextArea, str: []const u8, page: *Page) !void {
|
|||||||
try self.setValue(new_value, page);
|
try self.setValue(new_value, page);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
try self.dispatchInputEvent(str, "insertText", page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getSelectionDirection(self: *const TextArea) []const u8 {
|
pub fn getSelectionDirection(self: *const TextArea) []const u8 {
|
||||||
|
|||||||
121
src/browser/webapi/event/InputEvent.zig
Normal file
121
src/browser/webapi/event/InputEvent.zig
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const String = @import("../../../string.zig").String;
|
||||||
|
const Page = @import("../../Page.zig");
|
||||||
|
const Session = @import("../../Session.zig");
|
||||||
|
const js = @import("../../js/js.zig");
|
||||||
|
|
||||||
|
const Event = @import("../Event.zig");
|
||||||
|
const UIEvent = @import("UIEvent.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const InputEvent = @This();
|
||||||
|
|
||||||
|
_proto: *UIEvent,
|
||||||
|
_data: ?[]const u8,
|
||||||
|
// TODO: add dataTransfer
|
||||||
|
_input_type: []const u8,
|
||||||
|
_is_composing: bool,
|
||||||
|
|
||||||
|
pub const InputEventOptions = struct {
|
||||||
|
data: ?[]const u8 = null,
|
||||||
|
inputType: ?[]const u8 = null,
|
||||||
|
isComposing: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Options = Event.inheritOptions(
|
||||||
|
InputEvent,
|
||||||
|
InputEventOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*InputEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "InputEvent.trusted" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
return initWithTrusted(arena, typ, _opts, true, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*InputEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "InputEvent" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
const type_string = try String.init(arena, typ, .{});
|
||||||
|
return initWithTrusted(arena, type_string, _opts, false, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*InputEvent {
|
||||||
|
const opts = _opts orelse Options{};
|
||||||
|
|
||||||
|
const event = try page._factory.uiEvent(
|
||||||
|
arena,
|
||||||
|
typ,
|
||||||
|
InputEvent{
|
||||||
|
._proto = undefined,
|
||||||
|
._data = if (opts.data) |d| try arena.dupe(u8, d) else null,
|
||||||
|
._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else "",
|
||||||
|
._is_composing = opts.isComposing,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event
|
||||||
|
const rootevt = event._proto._proto;
|
||||||
|
rootevt._bubbles = true;
|
||||||
|
rootevt._cancelable = false;
|
||||||
|
rootevt._composed = true;
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {
|
||||||
|
self._proto.deinit(shutdown, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asEvent(self: *InputEvent) *Event {
|
||||||
|
return self._proto.asEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getData(self: *const InputEvent) ?[]const u8 {
|
||||||
|
return self._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getInputType(self: *const InputEvent) []const u8 {
|
||||||
|
return self._input_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getIsComposing(self: *const InputEvent) bool {
|
||||||
|
return self._is_composing;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const JsApi = struct {
|
||||||
|
pub const bridge = js.Bridge(InputEvent);
|
||||||
|
|
||||||
|
pub const Meta = struct {
|
||||||
|
pub const name = "InputEvent";
|
||||||
|
pub const prototype_chain = bridge.prototypeChain();
|
||||||
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
|
pub const weak = true;
|
||||||
|
pub const finalizer = bridge.finalizer(InputEvent.deinit);
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const constructor = bridge.constructor(InputEvent.init, .{});
|
||||||
|
pub const data = bridge.accessor(InputEvent.getData, null, .{});
|
||||||
|
pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{});
|
||||||
|
pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{});
|
||||||
|
};
|
||||||
@@ -219,6 +219,13 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
|
|||||||
);
|
);
|
||||||
|
|
||||||
Event.populatePrototypes(event, opts, trusted);
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
|
|
||||||
|
// https://w3c.github.io/uievents/#event-type-keyup
|
||||||
|
const rootevt = event._proto._proto;
|
||||||
|
rootevt._bubbles = true;
|
||||||
|
rootevt._cancelable = true;
|
||||||
|
rootevt._composed = true;
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const EventTarget = @import("../EventTarget.zig");
|
|||||||
const UIEvent = @import("UIEvent.zig");
|
const UIEvent = @import("UIEvent.zig");
|
||||||
const PointerEvent = @import("PointerEvent.zig");
|
const PointerEvent = @import("PointerEvent.zig");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const MouseEvent = @This();
|
const MouseEvent = @This();
|
||||||
|
|
||||||
pub const MouseButton = enum(u8) {
|
pub const MouseButton = enum(u8) {
|
||||||
@@ -83,12 +85,21 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
|
|||||||
const arena = try page.getArena(.{ .debug = "MouseEvent" });
|
const arena = try page.getArena(.{ .debug = "MouseEvent" });
|
||||||
errdefer page.releaseArena(arena);
|
errdefer page.releaseArena(arena);
|
||||||
const type_string = try String.init(arena, typ, .{});
|
const type_string = try String.init(arena, typ, .{});
|
||||||
|
return initWithTrusted(arena, type_string, _opts, false, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*MouseEvent {
|
||||||
|
const arena = try page.getArena(.{ .debug = "MouseEvent.trusted" });
|
||||||
|
errdefer page.releaseArena(arena);
|
||||||
|
return initWithTrusted(arena, typ, _opts, true, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*MouseEvent {
|
||||||
const opts = _opts orelse Options{};
|
const opts = _opts orelse Options{};
|
||||||
|
|
||||||
const event = try page._factory.uiEvent(
|
const event = try page._factory.uiEvent(
|
||||||
arena,
|
arena,
|
||||||
type_string,
|
typ,
|
||||||
MouseEvent{
|
MouseEvent{
|
||||||
._type = .generic,
|
._type = .generic,
|
||||||
._proto = undefined,
|
._proto = undefined,
|
||||||
@@ -106,7 +117,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Event.populatePrototypes(event, opts, false);
|
Event.populatePrototypes(event, opts, trusted);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub const Type = union(enum) {
|
|||||||
keyboard_event: *@import("KeyboardEvent.zig"),
|
keyboard_event: *@import("KeyboardEvent.zig"),
|
||||||
focus_event: *@import("FocusEvent.zig"),
|
focus_event: *@import("FocusEvent.zig"),
|
||||||
text_event: *@import("TextEvent.zig"),
|
text_event: *@import("TextEvent.zig"),
|
||||||
|
input_event: *@import("InputEvent.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const UIEventOptions = struct {
|
pub const UIEventOptions = struct {
|
||||||
@@ -88,6 +89,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T {
|
|||||||
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
.keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null,
|
||||||
.focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null,
|
.focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null,
|
||||||
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
|
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
|
||||||
|
.input_event => |e| return if (T == @import("InputEvent.zig")) e else null,
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ const js = @import("../../js/js.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const URL = @import("../../URL.zig");
|
const URL = @import("../../URL.zig");
|
||||||
|
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
const Request = @import("Request.zig");
|
const Request = @import("Request.zig");
|
||||||
const Response = @import("Response.zig");
|
const Response = @import("Response.zig");
|
||||||
|
const AbortSignal = @import("../AbortSignal.zig");
|
||||||
|
const DOMException = @import("../DOMException.zig");
|
||||||
|
|
||||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||||
|
|
||||||
@@ -38,17 +41,29 @@ _buf: std.ArrayList(u8),
|
|||||||
_response: *Response,
|
_response: *Response,
|
||||||
_resolver: js.PromiseResolver.Global,
|
_resolver: js.PromiseResolver.Global,
|
||||||
_owns_response: bool,
|
_owns_response: bool,
|
||||||
|
_signal: ?*AbortSignal,
|
||||||
|
|
||||||
pub const Input = Request.Input;
|
pub const Input = Request.Input;
|
||||||
pub const InitOpts = Request.InitOpts;
|
pub const InitOpts = Request.InitOpts;
|
||||||
|
|
||||||
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
||||||
const request = try Request.init(input, options, page);
|
const request = try Request.init(input, options, page);
|
||||||
|
const resolver = page.js.local.?.createPromiseResolver();
|
||||||
|
|
||||||
|
if (request._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
resolver.reject("fetch aborted", DOMException.init("The operation was aborted.", "AbortError"));
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, request._url, "blob:")) {
|
||||||
|
return handleBlobUrl(request._url, resolver, page);
|
||||||
|
}
|
||||||
|
|
||||||
const response = try Response.init(null, .{ .status = 0 }, page);
|
const response = try Response.init(null, .{ .status = 0 }, page);
|
||||||
errdefer response.deinit(true, page._session);
|
errdefer response.deinit(true, page._session);
|
||||||
|
|
||||||
const resolver = page.js.local.?.createPromiseResolver();
|
|
||||||
|
|
||||||
const fetch = try response._arena.create(Fetch);
|
const fetch = try response._arena.create(Fetch);
|
||||||
fetch.* = .{
|
fetch.* = .{
|
||||||
._page = page,
|
._page = page,
|
||||||
@@ -57,6 +72,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
|||||||
._resolver = try resolver.persist(),
|
._resolver = try resolver.persist(),
|
||||||
._response = response,
|
._response = response,
|
||||||
._owns_response = true,
|
._owns_response = true,
|
||||||
|
._signal = request._signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const http_client = page._session.browser.http_client;
|
const http_client = page._session.browser.http_client;
|
||||||
@@ -90,6 +106,26 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
|
|||||||
return resolver.promise();
|
return resolver.promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleBlobUrl(url: []const u8, resolver: js.PromiseResolver, page: *Page) !js.Promise {
|
||||||
|
const blob: *Blob = page.lookupBlobUrl(url) orelse {
|
||||||
|
resolver.rejectError("fetch blob error", .{ .type_error = "BlobNotFound" });
|
||||||
|
return resolver.promise();
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = try Response.init(null, .{ .status = 200 }, page);
|
||||||
|
response._body = try response._arena.dupe(u8, blob._slice);
|
||||||
|
response._url = try response._arena.dupeZ(u8, url);
|
||||||
|
response._type = .basic;
|
||||||
|
|
||||||
|
if (blob._mime.len > 0) {
|
||||||
|
try response._headers.append("Content-Type", blob._mime, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
const js_val = try page.js.local.?.zigValueToJs(response, .{});
|
||||||
|
resolver.resolve("fetch blob done", js_val);
|
||||||
|
return resolver.promise();
|
||||||
|
}
|
||||||
|
|
||||||
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -101,6 +137,12 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
|||||||
fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
if (self._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const arena = self._response._arena;
|
const arena = self._response._arena;
|
||||||
if (transfer.getContentLength()) |cl| {
|
if (transfer.getContentLength()) |cl| {
|
||||||
try self._buf.ensureTotalCapacity(arena, cl);
|
try self._buf.ensureTotalCapacity(arena, cl);
|
||||||
@@ -150,6 +192,14 @@ fn httpHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
|||||||
|
|
||||||
fn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
fn httpDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
|
||||||
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
const self: *Fetch = @ptrCast(@alignCast(transfer.ctx));
|
||||||
|
|
||||||
|
// Check if aborted
|
||||||
|
if (self._signal) |signal| {
|
||||||
|
if (signal._aborted) {
|
||||||
|
return error.Abort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try self._buf.appendSlice(self._response._arena, data);
|
try self._buf.appendSlice(self._response._arena, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +242,8 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
|||||||
self._page.js.localScope(&ls);
|
self._page.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
defer ls.deinit();
|
||||||
|
|
||||||
ls.toLocal(self._resolver).reject("fetch error", @errorName(err));
|
// fetch() must reject with a TypeError on network errors per spec
|
||||||
|
ls.toLocal(self._resolver).rejectError("fetch error", .{ .type_error = "fetch error" });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn httpShutdownCallback(ctx: *anyopaque) void {
|
fn httpShutdownCallback(ctx: *anyopaque) void {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const URL = @import("../URL.zig");
|
|||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
const Blob = @import("../Blob.zig");
|
const Blob = @import("../Blob.zig");
|
||||||
|
const AbortSignal = @import("../AbortSignal.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Request = @This();
|
const Request = @This();
|
||||||
@@ -36,6 +37,7 @@ _body: ?[]const u8,
|
|||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_cache: Cache,
|
_cache: Cache,
|
||||||
_credentials: Credentials,
|
_credentials: Credentials,
|
||||||
|
_signal: ?*AbortSignal,
|
||||||
|
|
||||||
pub const Input = union(enum) {
|
pub const Input = union(enum) {
|
||||||
request: *Request,
|
request: *Request,
|
||||||
@@ -48,6 +50,7 @@ pub const InitOpts = struct {
|
|||||||
body: ?[]const u8 = null,
|
body: ?[]const u8 = null,
|
||||||
cache: Cache = .default,
|
cache: Cache = .default,
|
||||||
credentials: Credentials = .@"same-origin",
|
credentials: Credentials = .@"same-origin",
|
||||||
|
signal: ?*AbortSignal = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Credentials = enum {
|
const Credentials = enum {
|
||||||
@@ -97,6 +100,13 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
|
|||||||
.request => |r| r._body,
|
.request => |r| r._body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const signal = if (opts.signal) |s|
|
||||||
|
s
|
||||||
|
else switch (input) {
|
||||||
|
.url => null,
|
||||||
|
.request => |r| r._signal,
|
||||||
|
};
|
||||||
|
|
||||||
return page._factory.create(Request{
|
return page._factory.create(Request{
|
||||||
._url = url,
|
._url = url,
|
||||||
._arena = arena,
|
._arena = arena,
|
||||||
@@ -105,6 +115,7 @@ pub fn init(input: Input, opts_: ?InitOpts, page: *Page) !*Request {
|
|||||||
._cache = opts.cache,
|
._cache = opts.cache,
|
||||||
._credentials = opts.credentials,
|
._credentials = opts.credentials,
|
||||||
._body = body,
|
._body = body,
|
||||||
|
._signal = signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +155,10 @@ pub fn getCredentials(self: *const Request) []const u8 {
|
|||||||
return @tagName(self._credentials);
|
return @tagName(self._credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getSignal(self: *const Request) ?*AbortSignal {
|
||||||
|
return self._signal;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getHeaders(self: *Request, page: *Page) !*Headers {
|
pub fn getHeaders(self: *Request, page: *Page) !*Headers {
|
||||||
if (self._headers) |headers| {
|
if (self._headers) |headers| {
|
||||||
return headers;
|
return headers;
|
||||||
@@ -177,8 +192,8 @@ pub fn text(self: *const Request, page: *Page) !js.Promise {
|
|||||||
pub fn json(self: *const Request, page: *Page) !js.Promise {
|
pub fn json(self: *const Request, page: *Page) !js.Promise {
|
||||||
const body = self._body orelse "";
|
const body = self._body orelse "";
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
const value = local.parseJSON(body) catch |err| {
|
const value = local.parseJSON(body) catch {
|
||||||
return local.rejectPromise(.{@errorName(err)});
|
return local.rejectPromise(.{ .syntax_error = "failed to parse" });
|
||||||
};
|
};
|
||||||
return local.resolvePromise(try value.persist());
|
return local.resolvePromise(try value.persist());
|
||||||
}
|
}
|
||||||
@@ -200,6 +215,7 @@ pub fn clone(self: *const Request, page: *Page) !*Request {
|
|||||||
._cache = self._cache,
|
._cache = self._cache,
|
||||||
._credentials = self._credentials,
|
._credentials = self._credentials,
|
||||||
._body = self._body,
|
._body = self._body,
|
||||||
|
._signal = self._signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +234,7 @@ pub const JsApi = struct {
|
|||||||
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
pub const headers = bridge.accessor(Request.getHeaders, null, .{});
|
||||||
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
pub const cache = bridge.accessor(Request.getCache, null, .{});
|
||||||
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
pub const credentials = bridge.accessor(Request.getCredentials, null, .{});
|
||||||
|
pub const signal = bridge.accessor(Request.getSignal, null, .{});
|
||||||
pub const blob = bridge.function(Request.blob, .{});
|
pub const blob = bridge.function(Request.blob, .{});
|
||||||
pub const text = bridge.function(Request.text, .{});
|
pub const text = bridge.function(Request.text, .{});
|
||||||
pub const json = bridge.function(Request.json, .{});
|
pub const json = bridge.function(Request.json, .{});
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ pub fn getText(self: *const Response, page: *Page) !js.Promise {
|
|||||||
pub fn getJson(self: *Response, page: *Page) !js.Promise {
|
pub fn getJson(self: *Response, page: *Page) !js.Promise {
|
||||||
const body = self._body orelse "";
|
const body = self._body orelse "";
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
const value = local.parseJSON(body) catch |err| {
|
const value = local.parseJSON(body) catch {
|
||||||
return local.rejectPromise(.{@errorName(err)});
|
return local.rejectPromise(.{ .syntax_error = "failed to parse" });
|
||||||
};
|
};
|
||||||
return local.resolvePromise(try value.persist());
|
return local.resolvePromise(try value.persist());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const Page = @import("../../Page.zig");
|
|||||||
const Session = @import("../../Session.zig");
|
const Session = @import("../../Session.zig");
|
||||||
|
|
||||||
const Node = @import("../Node.zig");
|
const Node = @import("../Node.zig");
|
||||||
|
const Blob = @import("../Blob.zig");
|
||||||
const Event = @import("../Event.zig");
|
const Event = @import("../Event.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
const EventTarget = @import("../EventTarget.zig");
|
const EventTarget = @import("../EventTarget.zig");
|
||||||
@@ -211,6 +212,11 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const page = self._page;
|
const page = self._page;
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, self._url, "blob:")) {
|
||||||
|
return self.handleBlobUrl(page);
|
||||||
|
}
|
||||||
|
|
||||||
const http_client = page._session.browser.http_client;
|
const http_client = page._session.browser.http_client;
|
||||||
var headers = try http_client.newHeaders();
|
var headers = try http_client.newHeaders();
|
||||||
|
|
||||||
@@ -242,6 +248,39 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
|
|||||||
|
|
||||||
page.js.strongRef(self);
|
page.js.strongRef(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
|
||||||
|
const blob = page.lookupBlobUrl(self._url) orelse {
|
||||||
|
self.handleError(error.BlobNotFound);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self._response_status = 200;
|
||||||
|
self._response_url = self._url;
|
||||||
|
|
||||||
|
try self._response_data.appendSlice(self._arena, blob._slice);
|
||||||
|
self._response_len = blob._slice.len;
|
||||||
|
|
||||||
|
try self.stateChanged(.headers_received, page);
|
||||||
|
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page);
|
||||||
|
try self.stateChanged(.loading, page);
|
||||||
|
try self._proto.dispatch(.progress, .{
|
||||||
|
.total = self._response_len orelse 0,
|
||||||
|
.loaded = self._response_data.items.len,
|
||||||
|
}, page);
|
||||||
|
try self.stateChanged(.done, page);
|
||||||
|
|
||||||
|
const loaded = self._response_data.items.len;
|
||||||
|
try self._proto.dispatch(.load, .{
|
||||||
|
.total = loaded,
|
||||||
|
.loaded = loaded,
|
||||||
|
}, page);
|
||||||
|
try self._proto.dispatch(.load_end, .{
|
||||||
|
.total = loaded,
|
||||||
|
.loaded = loaded,
|
||||||
|
}, page);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
|
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
|
||||||
return @intFromEnum(self._ready_state);
|
return @intFromEnum(self._ready_state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ pub fn pipeThrough(self: *ReadableStream, transform: PipeTransform, page: *Page)
|
|||||||
/// Returns a promise that resolves when piping is complete.
|
/// Returns a promise that resolves when piping is complete.
|
||||||
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
|
pub fn pipeTo(self: *ReadableStream, destination: *WritableStream, page: *Page) !js.Promise {
|
||||||
if (self.getLocked()) {
|
if (self.getLocked()) {
|
||||||
return page.js.local.?.rejectPromise("ReadableStream is locked");
|
return page.js.local.?.rejectPromise(.{ .type_error = "ReadableStream is locked" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const local = page.js.local.?;
|
const local = page.js.local.?;
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ pub const ReadResult = struct {
|
|||||||
|
|
||||||
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Reader has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state == .errored) {
|
if (stream._state == .errored) {
|
||||||
const err = stream._stored_error orelse "Stream errored";
|
//const err = stream._stored_error orelse "Stream errored";
|
||||||
return page.js.local.?.rejectPromise(err);
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream errored" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stream._controller.dequeue()) |chunk| {
|
if (stream._controller.dequeue()) |chunk| {
|
||||||
@@ -95,7 +95,7 @@ pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
|
|||||||
|
|
||||||
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
|
pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Reader has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Reader has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
self.releaseLock();
|
self.releaseLock();
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ pub fn init(stream: *WritableStream, page: *Page) !*WritableStreamDefaultWriter
|
|||||||
|
|
||||||
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
|
pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state != .writable) {
|
if (stream._state != .writable) {
|
||||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try stream.writeChunk(chunk, page);
|
try stream.writeChunk(chunk, page);
|
||||||
@@ -46,11 +46,11 @@ pub fn write(self: *WritableStreamDefaultWriter, chunk: js.Value, page: *Page) !
|
|||||||
|
|
||||||
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
pub fn close(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state != .writable) {
|
if (stream._state != .writable) {
|
||||||
return page.js.local.?.rejectPromise("Stream is not writable");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Stream is not writable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try stream.closeStream(page);
|
try stream.closeStream(page);
|
||||||
@@ -67,7 +67,7 @@ pub fn releaseLock(self: *WritableStreamDefaultWriter) void {
|
|||||||
|
|
||||||
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
pub fn getClosed(self: *WritableStreamDefaultWriter, page: *Page) !js.Promise {
|
||||||
const stream = self._stream orelse {
|
const stream = self._stream orelse {
|
||||||
return page.js.local.?.rejectPromise("Writer has been released");
|
return page.js.local.?.rejectPromise(.{ .type_error = "Writer has been released" });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (stream._state == .closed) {
|
if (stream._state == .closed) {
|
||||||
|
|||||||
@@ -228,6 +228,13 @@ pub const Writer = struct {
|
|||||||
|
|
||||||
try w.objectField("value");
|
try w.objectField("value");
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
.integer => |v| {
|
||||||
|
// CDP spec requires integer values to be serialized as strings.
|
||||||
|
// 20 bytes is enough for the decimal representation of a 64-bit integer.
|
||||||
|
var buf: [20]u8 = undefined;
|
||||||
|
const s = try std.fmt.bufPrint(&buf, "{d}", .{v});
|
||||||
|
try w.write(s);
|
||||||
|
},
|
||||||
inline else => |v| try w.write(v),
|
inline else => |v| try w.write(v),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1212,4 +1219,25 @@ test "AXNode: writer" {
|
|||||||
// Check childIds array exists
|
// Check childIds array exists
|
||||||
const child_ids = doc_node.get("childIds").?.array.items;
|
const child_ids = doc_node.get("childIds").?.array.items;
|
||||||
try testing.expect(child_ids.len > 0);
|
try testing.expect(child_ids.len > 0);
|
||||||
|
|
||||||
|
// Find the h1 node and verify its level property is serialized as a string
|
||||||
|
for (nodes) |node_val| {
|
||||||
|
const obj = node_val.object;
|
||||||
|
const role_obj = obj.get("role") orelse continue;
|
||||||
|
const role_val = role_obj.object.get("value") orelse continue;
|
||||||
|
if (!std.mem.eql(u8, role_val.string, "heading")) continue;
|
||||||
|
|
||||||
|
const props = obj.get("properties").?.array.items;
|
||||||
|
for (props) |prop| {
|
||||||
|
const prop_obj = prop.object;
|
||||||
|
const name_str = prop_obj.get("name").?.string;
|
||||||
|
if (!std.mem.eql(u8, name_str, "level")) continue;
|
||||||
|
const level_value = prop_obj.get("value").?.object;
|
||||||
|
try testing.expectEqual("integer", level_value.get("type").?.string);
|
||||||
|
// CDP spec: integer values must be serialized as strings
|
||||||
|
try testing.expectEqual("1", level_value.get("value").?.string);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error.HeadingNodeNotFound;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,13 +168,11 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
|
|
||||||
if (is_startup) {
|
if (is_startup) {
|
||||||
dispatchStartupCommand(&command, input.method) catch |err| {
|
dispatchStartupCommand(&command, input.method) catch |err| {
|
||||||
command.sendError(-31999, @errorName(err), .{}) catch {};
|
command.sendError(-31999, @errorName(err), .{}) catch return err;
|
||||||
return err;
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
dispatchCommand(&command, input.method) catch |err| {
|
dispatchCommand(&command, input.method) catch |err| {
|
||||||
command.sendError(-31998, @errorName(err), .{}) catch {};
|
command.sendError(-31998, @errorName(err), .{}) catch return err;
|
||||||
return err;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,18 +922,20 @@ test "cdp: invalid json" {
|
|||||||
// method is required
|
// method is required
|
||||||
try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));
|
try testing.expectError(error.InvalidJSON, ctx.processMessage(.{}));
|
||||||
|
|
||||||
try testing.expectError(error.InvalidMethod, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.method = "Target",
|
.method = "Target",
|
||||||
}));
|
});
|
||||||
try ctx.expectSentError(-31998, "InvalidMethod", .{});
|
try ctx.expectSentError(-31998, "InvalidMethod", .{});
|
||||||
|
|
||||||
try testing.expectError(error.UnknownDomain, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.method = "Unknown.domain",
|
.method = "Unknown.domain",
|
||||||
}));
|
});
|
||||||
|
try ctx.expectSentError(-31998, "UnknownDomain", .{});
|
||||||
|
|
||||||
try testing.expectError(error.UnknownMethod, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.method = "Target.over9000",
|
.method = "Target.over9000",
|
||||||
}));
|
});
|
||||||
|
try ctx.expectSentError(-31998, "UnknownMethod", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
test "cdp: invalid sessionId" {
|
test "cdp: invalid sessionId" {
|
||||||
|
|||||||
@@ -550,11 +550,12 @@ test "cdp.dom: getSearchResults unknown search id" {
|
|||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
defer ctx.deinit();
|
defer ctx.deinit();
|
||||||
|
|
||||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.id = 8,
|
.id = 8,
|
||||||
.method = "DOM.getSearchResults",
|
.method = "DOM.getSearchResults",
|
||||||
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
|
.params = .{ .searchId = "Nope", .fromIndex = 0, .toIndex = 10 },
|
||||||
}));
|
});
|
||||||
|
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 8 });
|
||||||
}
|
}
|
||||||
|
|
||||||
test "cdp.dom: search flow" {
|
test "cdp.dom: search flow" {
|
||||||
@@ -604,11 +605,12 @@ test "cdp.dom: search flow" {
|
|||||||
try ctx.expectSentResult(null, .{ .id = 16 });
|
try ctx.expectSentResult(null, .{ .id = 16 });
|
||||||
|
|
||||||
// make sure the delete actually did something
|
// make sure the delete actually did something
|
||||||
try testing.expectError(error.SearchResultNotFound, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.id = 17,
|
.id = 17,
|
||||||
.method = "DOM.getSearchResults",
|
.method = "DOM.getSearchResults",
|
||||||
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
.params = .{ .searchId = "0", .fromIndex = 0, .toIndex = 1 },
|
||||||
}));
|
});
|
||||||
|
try ctx.expectSentError(-31998, "SearchResultNotFound", .{ .id = 17 });
|
||||||
}
|
}
|
||||||
|
|
||||||
test "cdp.dom: querySelector unknown search id" {
|
test "cdp.dom: querySelector unknown search id" {
|
||||||
@@ -645,11 +647,12 @@ test "cdp.dom: querySelector Node not found" {
|
|||||||
});
|
});
|
||||||
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 });
|
try ctx.expectSentResult(.{ .searchId = "0", .resultCount = 2 }, .{ .id = 3 });
|
||||||
|
|
||||||
try testing.expectError(error.NodeNotFoundForGivenId, ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.id = 4,
|
.id = 4,
|
||||||
.method = "DOM.querySelector",
|
.method = "DOM.querySelector",
|
||||||
.params = .{ .nodeId = 1, .selector = "a" },
|
.params = .{ .nodeId = 1, .selector = "a" },
|
||||||
}));
|
});
|
||||||
|
try ctx.expectSentError(-31998, "NodeNotFoundForGivenId", .{ .id = 4 });
|
||||||
|
|
||||||
try ctx.processMessage(.{
|
try ctx.processMessage(.{
|
||||||
.id = 5,
|
.id = 5,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
@@ -24,6 +25,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
setFocusEmulationEnabled,
|
setFocusEmulationEnabled,
|
||||||
setDeviceMetricsOverride,
|
setDeviceMetricsOverride,
|
||||||
setTouchEmulationEnabled,
|
setTouchEmulationEnabled,
|
||||||
|
setUserAgentOverride,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -31,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
.setFocusEmulationEnabled => return setFocusEmulationEnabled(cmd),
|
||||||
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
.setDeviceMetricsOverride => return setDeviceMetricsOverride(cmd),
|
||||||
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
.setTouchEmulationEnabled => return setTouchEmulationEnabled(cmd),
|
||||||
|
.setUserAgentOverride => return setUserAgentOverride(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,3 +67,8 @@ fn setDeviceMetricsOverride(cmd: anytype) !void {
|
|||||||
fn setTouchEmulationEnabled(cmd: anytype) !void {
|
fn setTouchEmulationEnabled(cmd: anytype) !void {
|
||||||
return cmd.sendResult(null, .{});
|
return cmd.sendResult(null, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setUserAgentOverride(cmd: anytype) !void {
|
||||||
|
log.info(.app, "setUserAgentOverride ignored", .{});
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ const std = @import("std");
|
|||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
enable,
|
enable,
|
||||||
|
disable,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.enable => return cmd.sendResult(null, .{}),
|
.enable => return cmd.sendResult(null, .{}),
|
||||||
|
.disable => return cmd.sendResult(null, .{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
getSemanticTree,
|
getSemanticTree,
|
||||||
getInteractiveElements,
|
getInteractiveElements,
|
||||||
getStructuredData,
|
getStructuredData,
|
||||||
|
clickNode,
|
||||||
|
fillNode,
|
||||||
|
scrollNode,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -39,6 +42,9 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.getSemanticTree => return getSemanticTree(cmd),
|
.getSemanticTree => return getSemanticTree(cmd),
|
||||||
.getInteractiveElements => return getInteractiveElements(cmd),
|
.getInteractiveElements => return getInteractiveElements(cmd),
|
||||||
.getStructuredData => return getStructuredData(cmd),
|
.getStructuredData => return getStructuredData(cmd),
|
||||||
|
.clickNode => return clickNode(cmd),
|
||||||
|
.fillNode => return fillNode(cmd),
|
||||||
|
.scrollNode => return scrollNode(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,24 +52,32 @@ fn getSemanticTree(cmd: anytype) !void {
|
|||||||
const Params = struct {
|
const Params = struct {
|
||||||
format: ?enum { text } = null,
|
format: ?enum { text } = null,
|
||||||
prune: ?bool = null,
|
prune: ?bool = null,
|
||||||
|
interactiveOnly: ?bool = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
maxDepth: ?u32 = null,
|
||||||
};
|
};
|
||||||
const params = (try cmd.params(Params)) orelse Params{};
|
const params = (try cmd.params(Params)) orelse Params{};
|
||||||
|
|
||||||
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
const dom_node = page.document.asNode();
|
|
||||||
|
const dom_node = if (params.backendNodeId) |nodeId|
|
||||||
|
(bc.node_registry.lookup_by_id.get(nodeId) orelse return error.InvalidNodeId).dom
|
||||||
|
else
|
||||||
|
page.document.asNode();
|
||||||
|
|
||||||
var st = SemanticTree{
|
var st = SemanticTree{
|
||||||
.dom_node = dom_node,
|
.dom_node = dom_node,
|
||||||
.registry = &bc.node_registry,
|
.registry = &bc.node_registry,
|
||||||
.page = page,
|
.page = page,
|
||||||
.arena = cmd.arena,
|
.arena = cmd.arena,
|
||||||
.prune = params.prune orelse false,
|
.prune = params.prune orelse true,
|
||||||
|
.interactive_only = params.interactiveOnly orelse false,
|
||||||
|
.max_depth = params.maxDepth orelse std.math.maxInt(u32) - 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.format) |format| {
|
if (params.format) |format| {
|
||||||
if (format == .text) {
|
if (format == .text) {
|
||||||
st.prune = params.prune orelse true;
|
|
||||||
var aw: std.Io.Writer.Allocating = .init(cmd.arena);
|
var aw: std.Io.Writer.Allocating = .init(cmd.arena);
|
||||||
defer aw.deinit();
|
defer aw.deinit();
|
||||||
try st.textStringify(&aw.writer);
|
try st.textStringify(&aw.writer);
|
||||||
@@ -146,6 +160,76 @@ fn getStructuredData(cmd: anytype) !void {
|
|||||||
}, .{});
|
}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clickNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
|
||||||
|
lp.actions.click(node.dom, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fillNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
text: []const u8,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const node_id = params.nodeId orelse params.backendNodeId orelse return error.InvalidParam;
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
|
||||||
|
lp.actions.fill(node.dom, params.text, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrollNode(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
nodeId: ?Node.Id = null,
|
||||||
|
backendNodeId: ?Node.Id = null,
|
||||||
|
x: ?i32 = null,
|
||||||
|
y: ?i32 = null,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const maybe_node_id = params.nodeId orelse params.backendNodeId;
|
||||||
|
|
||||||
|
var target_node: ?*DOMNode = null;
|
||||||
|
if (maybe_node_id) |node_id| {
|
||||||
|
const node = bc.node_registry.lookup_by_id.get(node_id) orelse return error.InvalidNodeId;
|
||||||
|
target_node = node.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
lp.actions.scroll(target_node, params.x, params.y, page) catch |err| {
|
||||||
|
if (err == error.InvalidNodeType) return error.InvalidParam;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmd.sendResult(.{}, .{});
|
||||||
|
}
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "cdp.lp: getMarkdown" {
|
test "cdp.lp: getMarkdown" {
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
@@ -195,3 +279,63 @@ test "cdp.lp: getStructuredData" {
|
|||||||
const result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
const result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
||||||
try testing.expect(result.get("structuredData") != null);
|
try testing.expect(result.get("structuredData") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.lp: action tools" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
const bc = try ctx.loadBrowserContext(.{});
|
||||||
|
const page = try bc.session.createPage();
|
||||||
|
const url = "http://localhost:9582/src/browser/tests/mcp_actions.html";
|
||||||
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
|
_ = bc.session.wait(5000);
|
||||||
|
|
||||||
|
// Test Click
|
||||||
|
const btn = page.document.getElementById("btn", page).?.asNode();
|
||||||
|
const btn_id = (try bc.node_registry.register(btn)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 1,
|
||||||
|
.method = "LP.clickNode",
|
||||||
|
.params = .{ .backendNodeId = btn_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Fill Input
|
||||||
|
const inp = page.document.getElementById("inp", page).?.asNode();
|
||||||
|
const inp_id = (try bc.node_registry.register(inp)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 2,
|
||||||
|
.method = "LP.fillNode",
|
||||||
|
.params = .{ .backendNodeId = inp_id, .text = "hello" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Fill Select
|
||||||
|
const sel = page.document.getElementById("sel", page).?.asNode();
|
||||||
|
const sel_id = (try bc.node_registry.register(sel)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 3,
|
||||||
|
.method = "LP.fillNode",
|
||||||
|
.params = .{ .backendNodeId = sel_id, .text = "opt2" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Scroll
|
||||||
|
const scrollbox = page.document.getElementById("scrollbox", page).?.asNode();
|
||||||
|
const scrollbox_id = (try bc.node_registry.register(scrollbox)).id;
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 4,
|
||||||
|
.method = "LP.scrollNode",
|
||||||
|
.params = .{ .backendNodeId = scrollbox_id, .y = 50 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluate assertions
|
||||||
|
var ls: lp.js.Local.Scope = undefined;
|
||||||
|
page.js.localScope(&ls);
|
||||||
|
defer ls.deinit();
|
||||||
|
|
||||||
|
var try_catch: lp.js.TryCatch = undefined;
|
||||||
|
try_catch.init(&ls.local);
|
||||||
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
const result = try ls.local.compileAndRun("window.clicked === true && window.inputVal === 'hello' && window.changed === true && window.selChanged === 'opt2' && window.scrolled === true", null);
|
||||||
|
|
||||||
|
try testing.expect(result.isTrue());
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user