mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
Merge branch 'main' into agent
This commit is contained in:
84
.github/workflows/e2e-test.yml
vendored
84
.github/workflows/e2e-test.yml
vendored
@@ -63,6 +63,25 @@ jobs:
|
||||
zig-out/bin/lightpanda
|
||||
retention-days: 1
|
||||
|
||||
cdpproxy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
repository: 'lightpanda-io/cdpproxy'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: build proxy
|
||||
run: go build
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cdpproxy
|
||||
path: cdpproxy
|
||||
retention-days: 1
|
||||
|
||||
demo-runner:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -73,7 +92,9 @@ jobs:
|
||||
robotstxt: [true, false]
|
||||
|
||||
name: demo-runner
|
||||
needs: zig-build-release
|
||||
needs:
|
||||
- zig-build-release
|
||||
- cdpproxy
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
@@ -86,19 +107,28 @@ jobs:
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
- name: download lightpanda release
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: download cdpproxy
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cdpproxy
|
||||
|
||||
- run: chmod a+x ./cdpproxy
|
||||
|
||||
- run: ./cdpproxy > cdp.log &
|
||||
|
||||
- if: matrix.proxy == true
|
||||
name: build and start proxy
|
||||
run: |
|
||||
cd proxy
|
||||
go build
|
||||
./proxy & echo $! > PROXY.id
|
||||
./proxy &
|
||||
|
||||
- if: matrix.cache == true
|
||||
run: mkdir /tmp/lp-cache
|
||||
@@ -120,17 +150,18 @@ jobs:
|
||||
echo "value=$args" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- run: |
|
||||
./lightpanda serve ${{ steps.args.outputs.value }} & echo $! > LPD.pid
|
||||
./lightpanda serve --port 9223 ${{ steps.args.outputs.value }} &
|
||||
|
||||
- run: |
|
||||
go run runner/main.go
|
||||
|
||||
- run: |
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- if: matrix.proxy == true
|
||||
run: |
|
||||
pkill proxy
|
||||
- name: upload cdp log
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: cdp-log-demo-runner-${{ matrix.proxy }}-${{ matrix.wba }}-${{ matrix.cache }}-${{ matrix.robotstxt }}
|
||||
path: cdp.log
|
||||
retention-days: 1
|
||||
|
||||
proxy-auth:
|
||||
strategy:
|
||||
@@ -140,8 +171,10 @@ jobs:
|
||||
cache: [true, false]
|
||||
robotstxt: [true, false]
|
||||
|
||||
name: demo-runner
|
||||
needs: zig-build-release
|
||||
name: proxy-auth
|
||||
needs:
|
||||
- zig-build-release
|
||||
- cdpproxy
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
@@ -154,18 +187,27 @@ jobs:
|
||||
|
||||
- run: npm install
|
||||
|
||||
- name: download artifact
|
||||
- name: download lightpanda release
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: lightpanda-build-release
|
||||
|
||||
- run: chmod a+x ./lightpanda
|
||||
|
||||
- name: download cdpproxy
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cdpproxy
|
||||
|
||||
- run: chmod a+x ./cdpproxy
|
||||
|
||||
- run: ./cdpproxy > cdp.log &
|
||||
|
||||
- name: build and start proxy
|
||||
run: |
|
||||
cd proxy
|
||||
go build
|
||||
./proxy & echo $! > PROXY.id
|
||||
./proxy &
|
||||
|
||||
- if: matrix.cache == true
|
||||
run: mkdir /tmp/lp-cache
|
||||
@@ -188,19 +230,23 @@ jobs:
|
||||
- name: run end to end tests through proxy
|
||||
run: |
|
||||
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||
./lightpanda serve --http-proxy http://127.0.0.1:3000 ${{ steps.args.outputs.value }} & echo $! > LPD.pid
|
||||
./lightpanda serve --port 9223 --http-proxy http://127.0.0.1:3000 ${{ steps.args.outputs.value }} &
|
||||
go run runner/main.go
|
||||
URL=https://demo-browser.lightpanda.io/campfire-commerce/ node puppeteer/proxy_auth.js
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- name: run request interception through proxy and playwright
|
||||
run: |
|
||||
export PROXY_USERNAME=username PROXY_PASSWORD=password
|
||||
./lightpanda serve ${{ steps.args.outputs.value }} & echo $! > LPD.pid
|
||||
./lightpanda serve --port 9223 ${{ steps.args.outputs.value }} &
|
||||
BASE_URL=https://demo-browser.lightpanda.io/ node playwright/proxy_auth.js
|
||||
kill `cat LPD.pid`
|
||||
|
||||
- run: pkill proxy
|
||||
- name: upload cdp log
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: cdp-log-proxy-auth-${{ matrix.wba }}-${{ matrix.cache }}-${{ matrix.robotstxt }}
|
||||
path: cdp.log
|
||||
retention-days: 1
|
||||
|
||||
wba-test:
|
||||
name: wba-test
|
||||
|
||||
@@ -96,6 +96,12 @@ frame: Frame,
|
||||
// to the original page like this.
|
||||
popups: std.ArrayList(*Frame) = .empty,
|
||||
|
||||
// Popups that have called window.close() but whose teardown is deferred to
|
||||
// Page.deinit. We can't deinit synchronously from window.close() because
|
||||
// that's invoked from JS still running on top of the Frame's V8 context (or
|
||||
// from a script eval whose parser still holds the Frame).
|
||||
queued_close: std.ArrayList(*Frame) = .empty,
|
||||
|
||||
// Initialize a Page and its root Frame.
|
||||
pub fn init(self: *Page, session: *Session, frame_id: u32) !void {
|
||||
const frame_arena = try session.arena_pool.acquire(.large, "Page.frame_arena");
|
||||
@@ -115,6 +121,11 @@ pub fn init(self: *Page, session: *Session, frame_id: u32) !void {
|
||||
// Tear down the Page and its root Frame. Equivalent to the old
|
||||
// Session.removePage + Session.resetFrameResources.
|
||||
pub fn deinit(self: *Page, abort_http: bool) void {
|
||||
for (self.queued_close.items) |popup| {
|
||||
popup.deinit(abort_http);
|
||||
}
|
||||
self.queued_close = .empty;
|
||||
|
||||
for (self.popups.items) |popup| {
|
||||
popup.deinit(abort_http);
|
||||
}
|
||||
|
||||
60
src/browser/tests/element/html/contenteditable.html
Normal file
60
src/browser/tests/element/html/contenteditable.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../../testing.js"></script>
|
||||
|
||||
<!--
|
||||
Lightpanda has no caret/keyboard editing pipeline, so isContentEditable
|
||||
always returns false regardless of the element's effective contenteditable
|
||||
state. The spec walk still runs internally to log .not_implemented when the
|
||||
spec would have said true; only the return value is asserted here. See
|
||||
Html.zig:getIsContentEditable and PR #2310 for context.
|
||||
-->
|
||||
|
||||
<div id="own-true" contenteditable="true">own true</div>
|
||||
<div id="own-empty" contenteditable="">own empty</div>
|
||||
<div id="own-false" contenteditable="false">own false</div>
|
||||
<div id="own-plaintext" contenteditable="plaintext-only">own plaintext-only</div>
|
||||
|
||||
<div id="ancestor-true" contenteditable="true">
|
||||
<span id="child-of-true">child</span>
|
||||
</div>
|
||||
|
||||
<div id="ancestor-false" contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<span id="grandchild-rehosted">re-hosted</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="plain"><span id="child-of-plain">no editing context</span></div>
|
||||
|
||||
<script id="always-false">
|
||||
{
|
||||
testing.expectEqual(false, document.getElementById('own-true').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('own-empty').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('own-false').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('own-plaintext').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('ancestor-true').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('child-of-true').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('ancestor-false').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('grandchild-rehosted').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('plain').isContentEditable);
|
||||
testing.expectEqual(false, document.getElementById('child-of-plain').isContentEditable);
|
||||
testing.expectEqual(false, document.body.isContentEditable);
|
||||
testing.expectEqual(false, document.documentElement.isContentEditable);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="dynamic-attribute">
|
||||
{
|
||||
const el = document.createElement('div');
|
||||
testing.expectEqual(false, el.isContentEditable);
|
||||
|
||||
el.setAttribute('contenteditable', 'true');
|
||||
testing.expectEqual(false, el.isContentEditable);
|
||||
|
||||
el.setAttribute('contenteditable', 'false');
|
||||
testing.expectEqual(false, el.isContentEditable);
|
||||
|
||||
el.removeAttribute('contenteditable');
|
||||
testing.expectEqual(false, el.isContentEditable);
|
||||
}
|
||||
</script>
|
||||
@@ -107,3 +107,27 @@
|
||||
testing.expectEqual(null, window.opener);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=popup_self_close type=module>
|
||||
{
|
||||
// Popup navigates to a real URL (not about:blank, which takes a synchronous
|
||||
// shortcut) and calls window.close() from inside its own running script.
|
||||
// Regression: synchronous frame teardown here used to destroy the popup's
|
||||
// V8 context mid-eval, panicking on the deferred load-event dispatch.
|
||||
// Receiving the postMessage proves the popup's script ran to completion
|
||||
// (including the close call) without crashing.
|
||||
const state = await testing.async();
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data && e.data.from === 'popup_self_close') {
|
||||
state.resolve(e.data);
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
const w = window.open(testing.BASE_URL + 'window/support/popup_self_close.html');
|
||||
testing.expectTrue(w != null);
|
||||
|
||||
await state.done((data) => {
|
||||
testing.expectEqual('popup_self_close', data.from);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
12
src/browser/tests/window/support/popup_self_close.html
Normal file
12
src/browser/tests/window/support/popup_self_close.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<script>
|
||||
// Post a message back to the opener, then close ourselves from inside the
|
||||
// popup's own running script. This exercises the deferred-close path:
|
||||
// close() is invoked while the popup's V8 context is still entered and the
|
||||
// parser is still on the stack. A synchronous teardown here would destroy
|
||||
// the context and crash the eval's deferred load-event dispatch.
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ from: 'popup_self_close' }, '*');
|
||||
}
|
||||
window.close();
|
||||
</script>
|
||||
@@ -568,7 +568,14 @@ pub fn close(self: *Window) void {
|
||||
}
|
||||
}
|
||||
|
||||
frame.deinit(true);
|
||||
// We can't tear the Frame down here — close() is invoked from JS still
|
||||
// running on top of this Frame's V8 context, often deep inside a script
|
||||
// eval whose parser is still holding the Frame. Destroying the context
|
||||
// now leaves dangling pointers in the unwinding script eval (load event
|
||||
// dispatch, runMacrotasks, etc.). Defer to Page.deinit instead.
|
||||
page.queued_close.append(page.frame_arena, frame) catch |err| {
|
||||
log.err(.frame, "queue popup close", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, frame: *Frame) !void {
|
||||
|
||||
@@ -382,6 +382,31 @@ pub fn setTitle(self: *HtmlElement, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("title"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
// HTML §7.7.5.2 specifies the IDL attribute as true iff the element's effective
|
||||
// content editable state is "true" or "plaintext-only". Lightpanda has no
|
||||
// caret/keyboard editing pipeline, so a true answer cannot be honored
|
||||
// end-to-end — downstream CDP tools (notably Puppeteer's dispatchKeyEvent
|
||||
// path) would route into an input pipeline that silently no-ops. Always
|
||||
// return false, and log .not_implemented when the spec would have said true
|
||||
// so usage surfaces in telemetry rather than silently depending on an
|
||||
// unsupported value. Spec walk per HTML §7.7.5.2 still applies — the nearest
|
||||
// ancestor with `contenteditable` wins; "false" disables. See PR #2310 for
|
||||
// the routing-vs-fail-loud discussion.
|
||||
//
|
||||
// "contenteditable" is 15 bytes — past the comptime SSO limit — so the
|
||||
// String wrap runs at runtime, mirroring the pattern in interactive.zig.
|
||||
pub fn getIsContentEditable(self: *HtmlElement) bool {
|
||||
var current: ?*Element = self.asElement();
|
||||
while (current) |el| : (current = el.parentElement()) {
|
||||
const raw = el.getAttributeSafe(.wrap("contenteditable")) orelse continue;
|
||||
if (!std.ascii.eqlIgnoreCase(raw, "false")) {
|
||||
log.info(.not_implemented, "IsContentEditable", .{});
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getAttributeFunction(
|
||||
self: *HtmlElement,
|
||||
listener_type: GlobalEventHandler,
|
||||
@@ -1220,6 +1245,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{});
|
||||
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
|
||||
pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{});
|
||||
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{});
|
||||
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
|
||||
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{});
|
||||
@@ -1357,3 +1383,6 @@ test "WebApi: HTML.event_listeners" {
|
||||
test "WebApi: HTMLElement.props" {
|
||||
try testing.htmlRunner("element/html/htmlelement-props.html", .{});
|
||||
}
|
||||
test "WebApi: HTMLElement.contenteditable" {
|
||||
try testing.htmlRunner("element/html/contenteditable.html", .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user