Compare commits

...

13 Commits

Author SHA1 Message Date
trop[bot]
d89b7f0a4e feat: allow headers to be sent with webContents.downloadURL() (#39560)
feat: allow headers to be sent with webContents.downloadURL()

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2023-08-24 11:04:25 -04:00
trop[bot]
dfbd4c4335 fix: ensure BrowserView bounds are always relative to window (#39627)
fix: ensure BrowserView bounds are always relative to window

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2023-08-24 10:46:08 -04:00
trop[bot]
d79189056d docs: mention alternative tooling (#39637)
* docs: mention alternative tooling

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* Update forge-overview.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

* Update forge-overview.md

Co-authored-by: Erick Zhao <erick@hotmail.ca>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Erick Zhao <erick@hotmail.ca>
2023-08-24 15:43:11 +02:00
trop[bot]
6fd069231f fix: instantiate tab video tracks from BrowserCaptureMediaStreamTrack (#39619)
return BrowserCaptureMediaStreamTrack instead of MediaStreamTrack

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: brhenrique <bruno.d@miro.com>
2023-08-23 22:13:08 +02:00
trop[bot]
56e749782e refactor: node::Environment self-cleanup (#39628)
* chore: savepoint

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: turn raw_ptr tests back off

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2023-08-23 12:25:20 -05:00
trop[bot]
864dd4af40 fix: chrome.tabs 'url' and 'title' are privileged information (#39608)
fix: tabs url and title are privileged information

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2023-08-22 23:02:08 +02:00
trop[bot]
3d31570f8d fix: dangling raw_ptr in ElectronBrowserMainParts dtor (#39593)
* fix: dangling raw_ptr in ElectronBrowserMainParts dtor

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* fixup! fix: dangling raw_ptr in ElectronBrowserMainParts dtor

Browser::WhenReady() holds a reference to JsEnv isolate so must come after

Co-authored-by: Charles Kerr <charles@charleskerr.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2023-08-21 15:54:31 +02:00
trop[bot]
3411959886 fix: chrome://gpu failing to load (#39583)
fix: chrome://gpu failing to load

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2023-08-21 15:54:17 +02:00
trop[bot]
b0b1f2c727 fix: use tiled edges to calculate frame inset sizes in Linux (#39570)
Adapt to the window frame size calculation changes in CL 3970920 by
setting the inset sizes to 0 for tiled edges.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Athul Iddya <athul@iddya.com>
2023-08-21 11:42:54 +09:00
trop[bot]
4128e9f0e0 refactor: prefer Sorted variant of MakeFixedFlatSet() (#39564)
perf: prefer Sorted variant of MakeFixedFlatSet()

https://chromium-review.googlesource.com/c/chromium/src/+/4660000
says that the sorted version is simpler at compile time because it
can skip MakeFixedFlatSet()'s compile-time dynamic sorting.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2023-08-21 10:06:15 +09:00
trop[bot]
0bd5e36411 docs: note macOS bounds Tray offset (#39552)
* docs: note macOS bounds Tray offset

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* Update docs/api/browser-window.md

Co-authored-by: David Sanders <dsanders11@ucsbalum.com>

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2023-08-17 13:57:32 +02:00
trop[bot]
c97a4ce691 fix: destruction order of js env fields (#39549)
isolate_ depends on isolate_holder_ and so must be destroyed first.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2023-08-17 11:03:03 +02:00
trop[bot]
fbb982350b docs: add missing webview render-process-gone event (#39543)
docs: add mising webview 'render-process-gone' event

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Milan Burda <milan.burda@gmail.com>
2023-08-17 10:33:11 +02:00
44 changed files with 877 additions and 468 deletions

View File

@@ -412,18 +412,7 @@ Returns:
* `event` Event
* `webContents` [WebContents](web-contents.md)
* `details` Object
* `reason` string - The reason the render process is gone. Possible values:
* `clean-exit` - Process exited with an exit code of zero
* `abnormal-exit` - Process exited with a non-zero exit code
* `killed` - Process was sent a SIGTERM or otherwise killed externally
* `crashed` - Process crashed
* `oom` - Process ran out of memory
* `launch-failed` - Process never successfully launched
* `integrity-failure` - Windows code integrity checks failed
* `exitCode` Integer - The exit code of the process, unless `reason` is
`launch-failed`, in which case `exitCode` will be a platform-specific
launch failure error code.
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
Emitted when the renderer process unexpectedly disappears. This is normally
because it was crashed or killed.

View File

@@ -820,10 +820,14 @@ win.setBounds({ width: 100 })
console.log(win.getBounds())
```
**Note:** On macOS, the y-coordinate value cannot be smaller than the [Tray](tray.md) height. The tray height has changed over time and depends on the operating system, but is between 20-40px. Passing a value lower than the tray height will result in a window that is flush to the tray.
#### `win.getBounds()`
Returns [`Rectangle`](structures/rectangle.md) - The `bounds` of the window as `Object`.
**Note:** On macOS, the y-coordinate value returned will be at minimum the [Tray](tray.md) height. For example, calling `win.setBounds({ x: 25, y: 20, width: 800, height: 600 })` with a tray height of 38 means that `win.getBounds()` will return `{ x: 25, y: 38, width: 800, height: 600 }`.
#### `win.getBackgroundColor()`
Returns `string` - Gets the background color of the window in Hex (`#RRGGBB`) format.

View File

@@ -1311,7 +1311,7 @@ The API will generate a [DownloadItem](download-item.md) that can be accessed
with the [will-download](#event-will-download) event.
**Note:** This does not perform any security checks that relate to a page's origin,
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl).
unlike [`webContents.downloadURL`](web-contents.md#contentsdownloadurlurl-options).
#### `ses.createInterruptedDownload(options)`

View File

@@ -0,0 +1,13 @@
# RenderProcessGoneDetails Object
* `reason` string - The reason the render process is gone. Possible values:
* `clean-exit` - Process exited with an exit code of zero
* `abnormal-exit` - Process exited with a non-zero exit code
* `killed` - Process was sent a SIGTERM or otherwise killed externally
* `crashed` - Process crashed
* `oom` - Process ran out of memory
* `launch-failed` - Process never successfully launched
* `integrity-failure` - Windows code integrity checks failed
* `exitCode` Integer - The exit code of the process, unless `reason` is
`launch-failed`, in which case `exitCode` will be a platform-specific
launch failure error code.

View File

@@ -479,18 +479,7 @@ checking `reason === 'killed'` when you switch to that event.
Returns:
* `event` Event
* `details` Object
* `reason` string - The reason the render process is gone. Possible values:
* `clean-exit` - Process exited with an exit code of zero
* `abnormal-exit` - Process exited with a non-zero exit code
* `killed` - Process was sent a SIGTERM or otherwise killed externally
* `crashed` - Process crashed
* `oom` - Process ran out of memory
* `launch-failed` - Process never successfully launched
* `integrity-failure` - Windows code integrity checks failed
* `exitCode` Integer - The exit code of the process, unless `reason` is
`launch-failed`, in which case `exitCode` will be a platform-specific
launch failure error code.
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
Emitted when the renderer process unexpectedly disappears. This is normally
because it was crashed or killed.
@@ -1057,9 +1046,11 @@ const win = new BrowserWindow()
win.loadFile('src/index.html')
```
#### `contents.downloadURL(url)`
#### `contents.downloadURL(url[, options])`
* `url` string
* `options` Object (optional)
* `headers` Record<string, string> (optional) - HTTP request headers.
Initiates a download of the resource at `url` without navigating. The
`will-download` event of `session` will be triggered.

View File

@@ -280,9 +280,11 @@ if the page fails to load (see
Loads the `url` in the webview, the `url` must contain the protocol prefix,
e.g. the `http://` or `file://`.
### `<webview>.downloadURL(url)`
### `<webview>.downloadURL(url[, options])`
* `url` string
* `options` Object (optional)
* `headers` Record<string, string> (optional) - HTTP request headers.
Initiates a download of the resource at `url` without navigating.
@@ -983,9 +985,22 @@ ipcRenderer.on('ping', () => {
})
```
### Event: 'crashed'
### Event: 'crashed' _Deprecated_
Fired when the renderer process is crashed.
Fired when the renderer process crashes or is killed.
**Deprecated:** This event is superceded by the `render-process-gone` event
which contains more information about why the render process disappeared. It
isn't always because it crashed.
### Event: 'render-process-gone'
Returns:
* `details` [RenderProcessGoneDetails](structures/render-process-gone-details.md)
Fired when the renderer process unexpectedly disappears. This is normally
because it was crashed or killed.
### Event: 'plugin-crashed'

View File

@@ -4,15 +4,48 @@ Electron Forge is a tool for packaging and publishing Electron applications.
It unifies Electron's build tooling ecosystem into
a single extensible interface so that anyone can jump right into making Electron apps.
<details>
<summary>Alternative tooling</summary>
If you do not want to use Electron Forge for your project, there are other
third-party tools you can use to distribute your app.
These tools are maintained by members of the Electron community,
and do not come with official support from the Electron project.
**Electron Builder**
A "complete solution to package and build a ready-for-distribution Electron app"
that focuses on an integrated experience. [`electron-builder`](https://github.com/electron-userland/electron-builder) adds a single dependency and manages all further requirements internally.
`electron-builder` replaces features and modules used by the Electron
maintainers (such as the auto-updater) with custom ones.
**Hydraulic Conveyor**
A [desktop app deployment tool](https://hydraulic.dev) that supports
cross-building/signing of all packages from any OS without the need for
multi-platform CI, can do synchronous web-style updates on each start
of the app, requires no code changes, can use plain HTTP servers for updates and
which focuses on ease of use. Conveyor replaces the Electron auto-updaters
with Sparkle on macOS, MSIX on Windows, and Linux package repositories.
Conveyor is a commercial tool that is free for open source projects. There's
an example of [how to package GitHub Desktop](https://hydraulic.dev/blog/8-packaging-electron-apps.html)
which can be used for learning.
</details>
## Getting started
The [Electron Forge docs][] contain detailed information on taking your application
from source code to your end users' machines.
This includes:
* Packaging your application [(package)][]
* Generating executables and installers for each OS [(make)][], and,
* Publishing these files to online platforms to download [(publish)][].
- Packaging your application [(package)][]
- Generating executables and installers for each OS [(make)][], and,
- Publishing these files to online platforms to download [(publish)][].
For beginners, we recommend following through Electron's [tutorial][] to develop, build,
package and publish your first Electron app. If you have already developed an app on your machine
@@ -20,11 +53,11 @@ and want to start on packaging and distribution, start from [step 5][] of the tu
## Getting help
* If you need help with developing your app, our [community Discord server][discord] is a great place
to get advice from other Electron app developers.
* If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
to see if any existing issues match your problem. If not, feel free to fill out our bug report
template and submit a new issue.
- If you need help with developing your app, our [community Discord server][discord] is a great place
to get advice from other Electron app developers.
- If you suspect you're running into a bug with Forge, please check the [GitHub issue tracker][]
to see if any existing issues match your problem. If not, feel free to fill out our bug report
template and submit a new issue.
[Electron Forge Docs]: https://www.electronforge.io/
[step 5]: ./tutorial-5-packaging.md

View File

@@ -61,6 +61,7 @@ template("electron_extra_paks") {
"$root_gen_dir/content/browser/tracing/tracing_resources.pak",
"$root_gen_dir/content/browser/webrtc/resources/webrtc_internals_resources.pak",
"$root_gen_dir/content/content_resources.pak",
"$root_gen_dir/content/gpu_resources.pak",
"$root_gen_dir/mojo/public/js/mojo_bindings_resources.pak",
"$root_gen_dir/net/net_resources.pak",
"$root_gen_dir/third_party/blink/public/resources/blink_resources.pak",
@@ -74,6 +75,7 @@ template("electron_extra_paks") {
"//chrome/common:resources",
"//components/resources",
"//content:content_resources",
"//content/browser/resources/gpu:resources",
"//content/browser/resources/media:resources",
"//content/browser/tracing:resources",
"//content/browser/webrtc/resources",

View File

@@ -115,6 +115,7 @@ auto_filenames = {
"docs/api/structures/protocol-response.md",
"docs/api/structures/rectangle.md",
"docs/api/structures/referrer.md",
"docs/api/structures/render-process-gone-details.md",
"docs/api/structures/resolved-endpoint.md",
"docs/api/structures/resolved-host.md",
"docs/api/structures/scrubber-item.md",

View File

@@ -54,13 +54,14 @@ namespace {
// See https://nodejs.org/api/cli.html#cli_options
void ExitIfContainsDisallowedFlags(const std::vector<std::string>& argv) {
// Options that are unilaterally disallowed.
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>({
"--enable-fips",
"--force-fips",
"--openssl-config",
"--use-bundled-ca",
"--use-openssl-ca",
});
static constexpr auto disallowed =
base::MakeFixedFlatSetSorted<base::StringPiece>({
"--enable-fips",
"--force-fips",
"--openssl-config",
"--use-bundled-ca",
"--use-openssl-ca",
});
for (const auto& arg : argv) {
const auto key = base::StringPiece(arg).substr(0, arg.find('='));

View File

@@ -768,10 +768,20 @@ void BaseWindow::AddBrowserView(gin::Handle<BrowserView> browser_view) {
browser_view->SetOwnerWindow(nullptr);
}
// If the user set the BrowserView's bounds before adding it to the window,
// we need to get those initial bounds *before* adding it to the window
// so bounds isn't returned relative despite not being correctly positioned
// relative to the window.
auto bounds = browser_view->GetBounds();
window_->AddBrowserView(browser_view->view());
window_->AddDraggableRegionProvider(browser_view.get());
browser_view->SetOwnerWindow(this);
browser_views_.emplace_back().Reset(isolate(), browser_view.ToV8());
// Recalibrate bounds relative to the containing window.
if (!bounds.IsEmpty())
browser_view->SetBounds(bounds);
}
}

View File

@@ -66,6 +66,9 @@ class BrowserView : public gin::Wrappable<BrowserView>,
BrowserView(const BrowserView&) = delete;
BrowserView& operator=(const BrowserView&) = delete;
gfx::Rect GetBounds();
void SetBounds(const gfx::Rect& bounds);
protected:
BrowserView(gin::Arguments* args, const gin_helper::Dictionary& options);
~BrowserView() override;
@@ -78,8 +81,6 @@ class BrowserView : public gin::Wrappable<BrowserView>,
private:
void SetAutoResize(AutoResizeFlags flags);
void SetBounds(const gfx::Rect& bounds);
gfx::Rect GetBounds();
void SetBackgroundColor(const std::string& color_name);
v8::Local<v8::Value> GetWebContents(v8::Isolate*);

View File

@@ -2444,12 +2444,25 @@ void WebContents::ReloadIgnoringCache() {
/* check_for_repost */ true);
}
void WebContents::DownloadURL(const GURL& url) {
auto* browser_context = web_contents()->GetBrowserContext();
auto* download_manager = browser_context->GetDownloadManager();
void WebContents::DownloadURL(const GURL& url, gin::Arguments* args) {
std::map<std::string, std::string> headers;
gin_helper::Dictionary options;
if (args->GetNext(&options)) {
if (options.Has("headers") && !options.Get("headers", &headers)) {
args->ThrowTypeError("Invalid value for headers - must be an object");
return;
}
}
std::unique_ptr<download::DownloadUrlParameters> download_params(
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
web_contents(), url, MISSING_TRAFFIC_ANNOTATION));
for (const auto& [name, value] : headers) {
download_params->add_request_header(name, value);
}
auto* download_manager =
web_contents()->GetBrowserContext()->GetDownloadManager();
download_manager->DownloadUrl(std::move(download_params));
}

View File

@@ -169,7 +169,7 @@ class WebContents : public ExclusiveAccessContext,
void LoadURL(const GURL& url, const gin_helper::Dictionary& options);
void Reload();
void ReloadIgnoringCache();
void DownloadURL(const GURL& url);
void DownloadURL(const GURL& url, gin::Arguments* args);
GURL GetURL() const;
std::u16string GetTitle() const;
bool IsLoading() const;

View File

@@ -580,7 +580,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
blink::MediaStreamDevice video_device(request.video_type, id, name);
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
nullptr, url::Origin::Create(request.security_origin),
content::DesktopMediaID::Parse(request.requested_video_device_id));
content::DesktopMediaID::Parse(video_device.id));
devices.video_device = video_device;
} else if (result_dict.Get("video", &rfh)) {
auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
@@ -592,7 +592,7 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
base::UTF16ToUTF8(web_contents->GetTitle()));
video_device.display_media_info = DesktopMediaIDToDisplayMediaInformation(
web_contents, url::Origin::Create(request.security_origin),
content::DesktopMediaID::Parse(request.requested_video_device_id));
content::DesktopMediaID::Parse(video_device.id));
devices.video_device = video_device;
} else {
gin_helper::ErrorThrower(args->isolate())

View File

@@ -204,11 +204,11 @@ ElectronBrowserMainParts* ElectronBrowserMainParts::self_ = nullptr;
ElectronBrowserMainParts::ElectronBrowserMainParts()
: fake_browser_process_(std::make_unique<BrowserProcessImpl>()),
browser_(std::make_unique<Browser>()),
node_bindings_(
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)),
electron_bindings_(
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())) {
node_bindings_{
NodeBindings::Create(NodeBindings::BrowserEnvironment::kBrowser)},
electron_bindings_{
std::make_unique<ElectronBindings>(node_bindings_->uv_loop())},
browser_{std::make_unique<Browser>()} {
DCHECK(!self_) << "Cannot have two ElectronBrowserMainParts";
self_ = this;
}
@@ -266,26 +266,25 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
node_bindings_->Initialize(js_env_->isolate()->GetCurrentContext());
// Create the global environment.
node::Environment* env = node_bindings_->CreateEnvironment(
node_env_ = node_bindings_->CreateEnvironment(
js_env_->isolate()->GetCurrentContext(), js_env_->platform());
node_env_ = std::make_unique<NodeEnvironment>(env);
env->set_trace_sync_io(env->options()->trace_sync_io);
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
// We do not want to crash the main process on unhandled rejections.
env->options()->unhandled_rejections = "warn-with-error-code";
node_env_->options()->unhandled_rejections = "warn-with-error-code";
// Add Electron extended APIs.
electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
electron_bindings_->BindTo(js_env_->isolate(), node_env_->process_object());
// Create explicit microtasks runner.
js_env_->CreateMicrotasksRunner();
// Wrap the uv loop with global env.
node_bindings_->set_uv_env(env);
node_bindings_->set_uv_env(node_env_.get());
// Load everything.
node_bindings_->LoadEnvironment(env);
node_bindings_->LoadEnvironment(node_env_.get());
// We already initialized the feature list in PreEarlyInitialization(), but
// the user JS script would not have had a chance to alter the command-line
@@ -627,9 +626,9 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
// Destroy node platform after all destructors_ are executed, as they may
// invoke Node/V8 APIs inside them.
node_env_->env()->set_trace_sync_io(false);
node_env_->set_trace_sync_io(false);
js_env_->DestroyMicrotasksRunner();
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
node_env_.reset();
auto default_context_key = ElectronBrowserContext::PartitionKey("", false);

View File

@@ -37,6 +37,10 @@ class Screen;
}
#endif
namespace node {
class Environment;
}
namespace ui {
class LinuxUiGetter;
}
@@ -47,7 +51,6 @@ class Browser;
class ElectronBindings;
class JavascriptEnvironment;
class NodeBindings;
class NodeEnvironment;
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
class ElectronExtensionsClient;
@@ -157,11 +160,20 @@ class ElectronBrowserMainParts : public content::BrowserMainParts {
// Before then, we just exit() without any intermediate steps.
absl::optional<int> exit_code_;
std::unique_ptr<JavascriptEnvironment> js_env_;
std::unique_ptr<Browser> browser_;
std::unique_ptr<NodeBindings> node_bindings_;
// depends-on: node_bindings_
std::unique_ptr<ElectronBindings> electron_bindings_;
std::unique_ptr<NodeEnvironment> node_env_;
// depends-on: node_bindings_
std::unique_ptr<JavascriptEnvironment> js_env_;
// depends-on: js_env_'s isolate
std::shared_ptr<node::Environment> node_env_;
// depends-on: js_env_'s isolate
std::unique_ptr<Browser> browser_;
std::unique_ptr<IconManager> icon_manager_;
std::unique_ptr<base::FieldTrialList> field_trial_list_;

View File

@@ -301,6 +301,7 @@ ExtensionFunction::ResponseAction TabsQueryFunction::Run() {
tabs::Tab tab;
tab.id = contents->ID();
tab.title = base::UTF16ToUTF8(wc->GetTitle());
tab.url = wc->GetLastCommittedURL().spec();
tab.active = contents->IsFocused();
tab.audible = contents->IsCurrentlyAudible();
@@ -322,12 +323,18 @@ ExtensionFunction::ResponseAction TabsGetFunction::Run() {
return RespondNow(Error("No such tab"));
tabs::Tab tab;
tab.id = tab_id;
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
// that have the "tabs" (or "activeTab") permission. We should do the same
// permission check here.
tab.url = contents->web_contents()->GetLastCommittedURL().spec();
// "title" and "url" properties are considered privileged data and can
// only be checked if the extension has the "tabs" permission or it has
// access to the WebContents's origin.
auto* wc = contents->web_contents();
if (extension()->permissions_data()->HasAPIPermissionForTab(
contents->ID(), mojom::APIPermissionID::kTab) ||
extension()->permissions_data()->HasHostPermission(wc->GetURL())) {
tab.url = wc->GetLastCommittedURL().spec();
tab.title = base::UTF16ToUTF8(wc->GetTitle());
}
tab.active = contents->IsFocused();
@@ -609,10 +616,16 @@ ExtensionFunction::ResponseValue TabsUpdateFunction::GetResult() {
auto* api_web_contents = electron::api::WebContents::From(web_contents_);
tab.id = (api_web_contents ? api_web_contents->ID() : -1);
// TODO(nornagon): in Chrome, the tab URL is only available to extensions
// that have the "tabs" (or "activeTab") permission. We should do the same
// permission check here.
tab.url = web_contents_->GetLastCommittedURL().spec();
// "title" and "url" properties are considered privileged data and can
// only be checked if the extension has the "tabs" permission or it has
// access to the WebContents's origin.
if (extension()->permissions_data()->HasAPIPermissionForTab(
api_web_contents->ID(), mojom::APIPermissionID::kTab) ||
extension()->permissions_data()->HasHostPermission(
web_contents_->GetURL())) {
tab.url = web_contents_->GetLastCommittedURL().spec();
tab.title = base::UTF16ToUTF8(web_contents_->GetTitle());
}
if (api_web_contents)
tab.active = api_web_contents->IsFocused();

View File

@@ -104,9 +104,10 @@ gin::IsolateHolder CreateIsolateHolder(v8::Isolate* isolate) {
JavascriptEnvironment::JavascriptEnvironment(uv_loop_t* event_loop,
bool setup_wasm_streaming)
: isolate_(Initialize(event_loop, setup_wasm_streaming)),
isolate_holder_(CreateIsolateHolder(isolate_)),
locker_(isolate_) {
: isolate_holder_{CreateIsolateHolder(
Initialize(event_loop, setup_wasm_streaming))},
isolate_{isolate_holder_.isolate()},
locker_{isolate_} {
isolate_->Enter();
v8::HandleScope scope(isolate_);
@@ -339,12 +340,4 @@ void JavascriptEnvironment::DestroyMicrotasksRunner() {
base::CurrentThread::Get()->RemoveTaskObserver(microtasks_runner_.get());
}
NodeEnvironment::NodeEnvironment(node::Environment* env) : env_(env) {}
NodeEnvironment::~NodeEnvironment() {
auto* isolate_data = env_->isolate_data();
node::FreeEnvironment(env_);
node::FreeIsolateData(isolate_data);
}
} // namespace electron

View File

@@ -43,29 +43,17 @@ class JavascriptEnvironment {
v8::Isolate* Initialize(uv_loop_t* event_loop, bool setup_wasm_streaming);
std::unique_ptr<node::MultiIsolatePlatform> platform_;
raw_ptr<v8::Isolate> isolate_;
gin::IsolateHolder isolate_holder_;
// owned-by: isolate_holder_
const raw_ptr<v8::Isolate> isolate_;
// depends-on: isolate_
v8::Locker locker_;
std::unique_ptr<MicrotasksRunner> microtasks_runner_;
};
// Manage the Node Environment automatically.
class NodeEnvironment {
public:
explicit NodeEnvironment(node::Environment* env);
~NodeEnvironment();
// disable copy
NodeEnvironment(const NodeEnvironment&) = delete;
NodeEnvironment& operator=(const NodeEnvironment&) = delete;
node::Environment* env() { return env_; }
private:
raw_ptr<node::Environment> env_;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_JAVASCRIPT_ENVIRONMENT_H_

View File

@@ -55,25 +55,27 @@ void NativeBrowserViewMac::SetBounds(const gfx::Rect& bounds) {
auto* iwc_view = GetInspectableWebContentsView();
if (!iwc_view)
return;
auto* view = iwc_view->GetNativeView().GetNativeNSView();
auto* superview = view.superview;
const auto superview_height = superview ? superview.frame.size.height : 0;
view.frame =
NSMakeRect(bounds.x(), superview_height - bounds.y() - bounds.height(),
bounds.width(), bounds.height());
const auto superview_height =
view.superview ? view.superview.frame.size.height : 0;
int y_coord = superview_height - bounds.y() - bounds.height();
view.frame = NSMakeRect(bounds.x(), y_coord, bounds.width(), bounds.height());
}
gfx::Rect NativeBrowserViewMac::GetBounds() {
auto* iwc_view = GetInspectableWebContentsView();
if (!iwc_view)
return gfx::Rect();
NSView* view = iwc_view->GetNativeView().GetNativeNSView();
const int superview_height =
(view.superview) ? view.superview.frame.size.height : 0;
return gfx::Rect(
view.frame.origin.x,
superview_height - view.frame.origin.y - view.frame.size.height,
view.frame.size.width, view.frame.size.height);
view.superview ? view.superview.frame.size.height : 0;
int y_coord = superview_height - view.frame.origin.y - view.frame.size.height;
return gfx::Rect(view.frame.origin.x, y_coord, view.frame.size.width,
view.frame.size.height);
}
void NativeBrowserViewMac::SetBackgroundColor(SkColor color) {

View File

@@ -150,7 +150,13 @@ void ClientFrameViewLinux::Init(NativeWindowViews* window,
}
gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
return frame_provider_->GetFrameThicknessDip();
const auto insets = frame_provider_->GetFrameThicknessDip();
// We shouldn't draw frame decorations for the tiled edges.
// See https://wayland.app/protocols/xdg-shell#xdg_toplevel:enum:state
return gfx::Insets::TLBR(tiled_edges().top ? 0 : insets.top(),
tiled_edges().left ? 0 : insets.left(),
tiled_edges().bottom ? 0 : insets.bottom(),
tiled_edges().right ? 0 : insets.right());
}
gfx::Insets ClientFrameViewLinux::GetInputInsets() const {

View File

@@ -45,7 +45,7 @@ electron::UsbChooserContext* GetChooserContext(
// These extensions can claim the smart card USB class and automatically gain
// permissions for devices that have an interface with this class.
constexpr auto kSmartCardPrivilegedExtensionIds =
base::MakeFixedFlatSet<base::StringPiece>({
base::MakeFixedFlatSetSorted<base::StringPiece>({
// Smart Card Connector Extension and its Beta version, see
// crbug.com/1233881.
"khpfeaanjngmcnplbdlpegiifgpfgdco",

View File

@@ -20,5 +20,11 @@
"extension_types": [
"extension"
]
},
"tabs": {
"channel": "stable",
"extension_types": [
"extension"
]
}
}

View File

@@ -39,6 +39,8 @@ constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
{mojom::APIPermissionID::kPdfViewerPrivate, "pdfViewerPrivate"},
#endif
{mojom::APIPermissionID::kManagement, "management"},
{mojom::APIPermissionID::kTab, "tabs",
APIPermissionInfo::kFlagRequiresManagementUIWarning},
};
base::span<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
return base::make_span(permissions_to_register);

View File

@@ -232,7 +232,7 @@ void ErrorMessageListener(v8::Local<v8::Message> message,
// If node CLI inspect support is disabled, allow no debug options.
bool IsAllowedOption(base::StringPiece option) {
static constexpr auto debug_options =
base::MakeFixedFlatSet<base::StringPiece>({
base::MakeFixedFlatSetSorted<base::StringPiece>({
"--debug",
"--debug-brk",
"--debug-port",
@@ -244,13 +244,14 @@ bool IsAllowedOption(base::StringPiece option) {
});
// This should be aligned with what's possible to set via the process object.
static constexpr auto options = base::MakeFixedFlatSet<base::StringPiece>({
"--trace-warnings",
"--trace-deprecation",
"--throw-deprecation",
"--no-deprecation",
"--dns-result-order",
});
static constexpr auto options =
base::MakeFixedFlatSetSorted<base::StringPiece>({
"--dns-result-order",
"--no-deprecation",
"--throw-deprecation",
"--trace-deprecation",
"--trace-warnings",
});
if (debug_options.contains(option))
return electron::fuses::IsNodeCliInspectEnabled();
@@ -262,14 +263,21 @@ bool IsAllowedOption(base::StringPiece option) {
// See https://nodejs.org/api/cli.html#cli_node_options_options
void SetNodeOptions(base::Environment* env) {
// Options that are unilaterally disallowed
static constexpr auto disallowed = base::MakeFixedFlatSet<base::StringPiece>(
{"--enable-fips", "--force-fips", "--openssl-config", "--use-bundled-ca",
"--use-openssl-ca", "--experimental-policy"});
static constexpr auto disallowed =
base::MakeFixedFlatSetSorted<base::StringPiece>({
"--enable-fips",
"--experimental-policy",
"--force-fips",
"--openssl-config",
"--use-bundled-ca",
"--use-openssl-ca",
});
static constexpr auto pkg_opts = base::MakeFixedFlatSet<base::StringPiece>({
"--http-parser",
"--max-http-header-size",
});
static constexpr auto pkg_opts =
base::MakeFixedFlatSetSorted<base::StringPiece>({
"--http-parser",
"--max-http-header-size",
});
if (env->HasVar("NODE_OPTIONS")) {
if (electron::fuses::IsNodeOptionsEnabled()) {
@@ -469,7 +477,7 @@ void NodeBindings::Initialize(v8::Local<v8::Context> context) {
g_is_initialized = true;
}
node::Environment* NodeBindings::CreateEnvironment(
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform,
std::vector<std::string> args,
@@ -636,10 +644,25 @@ node::Environment* NodeBindings::CreateEnvironment(
base::PathService::Get(content::CHILD_PROCESS_EXE, &helper_exec_path);
process.Set("helperExecPath", helper_exec_path);
return env;
auto env_deleter = [isolate, isolate_data,
context = v8::Global<v8::Context>{isolate, context}](
node::Environment* nenv) mutable {
// When `isolate_data` was created above, a pointer to it was kept
// in context's embedder_data[kElectronContextEmbedderDataIndex].
// Since we're about to free `isolate_data`, clear that entry
v8::HandleScope handle_scope{isolate};
context.Get(isolate)->SetAlignedPointerInEmbedderData(
kElectronContextEmbedderDataIndex, nullptr);
context.Reset();
node::FreeEnvironment(nenv);
node::FreeIsolateData(isolate_data);
};
return {env, std::move(env_deleter)};
}
node::Environment* NodeBindings::CreateEnvironment(
std::shared_ptr<node::Environment> NodeBindings::CreateEnvironment(
v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform) {
#if BUILDFLAG(IS_WIN)

View File

@@ -5,6 +5,7 @@
#ifndef ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
#define ELECTRON_SHELL_COMMON_NODE_BINDINGS_H_
#include <memory>
#include <string>
#include <type_traits>
#include <vector>
@@ -25,12 +26,6 @@ class SingleThreadTaskRunner;
namespace electron {
// Choose a reasonable unique index that's higher than any Blink uses
// and thus unlikely to collide with an existing index.
static constexpr int kElectronContextEmbedderDataIndex =
static_cast<int>(gin::kPerContextDataStartIndex) +
static_cast<int>(gin::kEmbedderElectron);
// A helper class to manage uv_handle_t types, e.g. uv_async_t.
//
// As per the uv docs: "uv_close() MUST be called on each handle before
@@ -95,12 +90,15 @@ class NodeBindings {
std::vector<std::string> ParseNodeCliFlags();
// Create the environment and load node.js.
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform,
std::vector<std::string> args,
std::vector<std::string> exec_args);
node::Environment* CreateEnvironment(v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform);
std::shared_ptr<node::Environment> CreateEnvironment(
v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform,
std::vector<std::string> args,
std::vector<std::string> exec_args);
std::shared_ptr<node::Environment> CreateEnvironment(
v8::Handle<v8::Context> context,
node::MultiIsolatePlatform* platform);
// Load node.js in the environment.
void LoadEnvironment(node::Environment* env);
@@ -111,12 +109,6 @@ class NodeBindings {
// Notify embed thread to start polling after environment is loaded.
void StartPolling();
// Clears the PerIsolateData.
void clear_isolate_data(v8::Local<v8::Context> context) {
context->SetAlignedPointerInEmbedderData(kElectronContextEmbedderDataIndex,
nullptr);
}
node::IsolateData* isolate_data(v8::Local<v8::Context> context) const {
if (context->GetNumberOfEmbedderDataFields() <=
kElectronContextEmbedderDataIndex) {
@@ -167,6 +159,12 @@ class NodeBindings {
raw_ptr<uv_loop_t> uv_loop_;
private:
// Choose a reasonable unique index that's higher than any Blink uses
// and thus unlikely to collide with an existing index.
static constexpr int kElectronContextEmbedderDataIndex =
static_cast<int>(gin::kPerContextDataStartIndex) +
static_cast<int>(gin::kEmbedderElectron);
// Thread to poll uv events.
static void EmbedThreadRunner(void* arg);

View File

@@ -88,7 +88,7 @@ void ElectronRendererClient::DidCreateScriptContext(
v8::Maybe<bool> initialized = node::InitializeContext(renderer_context);
CHECK(!initialized.IsNothing() && initialized.FromJust());
node::Environment* env =
std::shared_ptr<node::Environment> env =
node_bindings_->CreateEnvironment(renderer_context, nullptr);
// If we have disabled the site instance overrides we should prevent loading
@@ -106,11 +106,11 @@ void ElectronRendererClient::DidCreateScriptContext(
BindProcess(env->isolate(), &process_dict, render_frame);
// Load everything.
node_bindings_->LoadEnvironment(env);
node_bindings_->LoadEnvironment(env.get());
if (node_bindings_->uv_env() == nullptr) {
// Make uv loop being wrapped by window context.
node_bindings_->set_uv_env(env);
node_bindings_->set_uv_env(env.get());
// Give the node loop a run to make sure everything is ready.
node_bindings_->StartPolling();
@@ -124,7 +124,9 @@ void ElectronRendererClient::WillReleaseScriptContext(
return;
node::Environment* env = node::Environment::GetCurrent(context);
if (environments_.erase(env) == 0)
const auto iter = base::ranges::find_if(
environments_, [env](auto& item) { return env == item.get(); });
if (iter == environments_.end())
return;
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
@@ -143,9 +145,7 @@ void ElectronRendererClient::WillReleaseScriptContext(
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
node::FreeEnvironment(env);
node::FreeIsolateData(node_bindings_->isolate_data(context));
node_bindings_->clear_isolate_data(context);
environments_.erase(iter);
microtask_queue->set_microtasks_policy(old_policy);
@@ -201,7 +201,11 @@ node::Environment* ElectronRendererClient::GetEnvironment(
auto context =
GetContext(render_frame->GetWebFrame(), v8::Isolate::GetCurrent());
node::Environment* env = node::Environment::GetCurrent(context);
return base::Contains(environments_, env) ? env : nullptr;
return base::Contains(environments_, env,
[](auto const& item) { return item.get(); })
? env
: nullptr;
}
} // namespace electron

View File

@@ -55,7 +55,7 @@ class ElectronRendererClient : public RendererClientBase {
// The node::Environment::GetCurrent API does not return nullptr when it
// is called for a context without node::Environment, so we have to keep
// a book of the environments created.
std::set<node::Environment*> environments_;
std::set<std::shared_ptr<node::Environment>> environments_;
// Getting main script context from web frame would lazily initializes
// its script context. Doing so in a web page without scripts would trigger

View File

@@ -6,7 +6,9 @@
#include <utility>
#include "base/containers/cxx20_erase_set.h"
#include "base/no_destructor.h"
#include "base/ranges/algorithm.h"
#include "base/threading/thread_local.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
@@ -61,20 +63,23 @@ void WebWorkerObserver::WorkerScriptReadyForEvaluation(
// Setup node environment for each window.
v8::Maybe<bool> initialized = node::InitializeContext(worker_context);
CHECK(!initialized.IsNothing() && initialized.FromJust());
node::Environment* env =
std::shared_ptr<node::Environment> env =
node_bindings_->CreateEnvironment(worker_context, nullptr);
// Add Electron extended APIs.
electron_bindings_->BindTo(env->isolate(), env->process_object());
// Load everything.
node_bindings_->LoadEnvironment(env);
node_bindings_->LoadEnvironment(env.get());
// Make uv loop being wrapped by window context.
node_bindings_->set_uv_env(env);
node_bindings_->set_uv_env(env.get());
// Give the node loop a run to make sure everything is ready.
node_bindings_->StartPolling();
// Keep the environment alive until we free it in ContextWillDestroy()
environments_.insert(std::move(env));
}
void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
@@ -91,9 +96,8 @@ void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
DCHECK_EQ(microtask_queue->GetMicrotasksScopeDepth(), 0);
microtask_queue->set_microtasks_policy(v8::MicrotasksPolicy::kExplicit);
node::FreeEnvironment(env);
node::FreeIsolateData(node_bindings_->isolate_data(context));
node_bindings_->clear_isolate_data(context);
base::EraseIf(environments_,
[env](auto const& item) { return item.get() == env; });
microtask_queue->set_microtasks_policy(old_policy);

View File

@@ -6,9 +6,16 @@
#define ELECTRON_SHELL_RENDERER_WEB_WORKER_OBSERVER_H_
#include <memory>
#include <set>
#include "v8/include/v8.h"
namespace node {
class Environment;
} // namespace node
namespace electron {
class ElectronBindings;
@@ -35,6 +42,7 @@ class WebWorkerObserver {
private:
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<ElectronBindings> electron_bindings_;
std::set<std::shared_ptr<node::Environment>> environments_;
};
} // namespace electron

View File

@@ -30,9 +30,9 @@ NodeService::NodeService(
NodeService::~NodeService() {
if (!node_env_stopped_) {
node_env_->env()->set_trace_sync_io(false);
node_env_->set_trace_sync_io(false);
js_env_->DestroyMicrotasksRunner();
node::Stop(node_env_->env(), node::StopFlags::kDoNotTerminateIsolate);
node::Stop(node_env_.get(), node::StopFlags::kDoNotTerminateIsolate);
}
}
@@ -57,13 +57,12 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
#endif
// Create the global environment.
node::Environment* env = node_bindings_->CreateEnvironment(
node_env_ = node_bindings_->CreateEnvironment(
js_env_->isolate()->GetCurrentContext(), js_env_->platform(),
params->args, params->exec_args);
node_env_ = std::make_unique<NodeEnvironment>(env);
node::SetProcessExitHandler(
env, [this](node::Environment* env, int exit_code) {
node_env_.get(), [this](node::Environment* env, int exit_code) {
// Destroy node platform.
env->set_trace_sync_io(false);
js_env_->DestroyMicrotasksRunner();
@@ -72,20 +71,21 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
receiver_.ResetWithReason(exit_code, "");
});
env->set_trace_sync_io(env->options()->trace_sync_io);
node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);
// Add Electron extended APIs.
electron_bindings_->BindTo(env->isolate(), env->process_object());
electron_bindings_->BindTo(node_env_->isolate(), node_env_->process_object());
// Add entry script to process object.
gin_helper::Dictionary process(env->isolate(), env->process_object());
gin_helper::Dictionary process(node_env_->isolate(),
node_env_->process_object());
process.SetHidden("_serviceStartupScript", params->script);
// Setup microtask runner.
js_env_->CreateMicrotasksRunner();
// Wrap the uv loop with global env.
node_bindings_->set_uv_env(env);
node_bindings_->set_uv_env(node_env_.get());
// LoadEnvironment should be called after setting up
// JavaScriptEnvironment including the microtask runner
@@ -94,7 +94,7 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
// the exit handler set above will be triggered and it expects
// both Node Env and JavaScriptEnviroment are setup to perform
// a clean shutdown of this process.
node_bindings_->LoadEnvironment(env);
node_bindings_->LoadEnvironment(node_env_.get());
// Run entry script.
node_bindings_->PrepareEmbedThread();

View File

@@ -11,12 +11,17 @@
#include "mojo/public/cpp/bindings/receiver.h"
#include "shell/services/node/public/mojom/node_service.mojom.h"
namespace node {
class Environment;
} // namespace node
namespace electron {
class ElectronBindings;
class JavascriptEnvironment;
class NodeBindings;
class NodeEnvironment;
class NodeService : public node::mojom::NodeService {
public:
@@ -35,7 +40,7 @@ class NodeService : public node::mojom::NodeService {
std::unique_ptr<JavascriptEnvironment> js_env_;
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<ElectronBindings> electron_bindings_;
std::unique_ptr<NodeEnvironment> node_env_;
std::shared_ptr<node::Environment> node_env_;
mojo::Receiver<node::mojom::NodeService> receiver_{this};
};

View File

@@ -147,6 +147,41 @@ describe('BrowserView module', () => {
view.setBounds({} as any);
}).to.throw(/conversion failure/);
});
it('can set bounds after view is added to window', () => {
view = new BrowserView();
const bounds = { x: 0, y: 0, width: 50, height: 50 };
w.addBrowserView(view);
view.setBounds(bounds);
expect(view.getBounds()).to.deep.equal(bounds);
});
it('can set bounds before view is added to window', () => {
view = new BrowserView();
const bounds = { x: 0, y: 0, width: 50, height: 50 };
view.setBounds(bounds);
w.addBrowserView(view);
expect(view.getBounds()).to.deep.equal(bounds);
});
it('can update bounds', () => {
view = new BrowserView();
w.addBrowserView(view);
const bounds1 = { x: 0, y: 0, width: 50, height: 50 };
view.setBounds(bounds1);
expect(view.getBounds()).to.deep.equal(bounds1);
const bounds2 = { x: 0, y: 150, width: 50, height: 50 };
view.setBounds(bounds2);
expect(view.getBounds()).to.deep.equal(bounds2);
});
});
describe('BrowserView.getBounds()', () => {
@@ -156,6 +191,16 @@ describe('BrowserView module', () => {
view.setBounds(bounds);
expect(view.getBounds()).to.deep.equal(bounds);
});
it('does not changer after being added to a window', () => {
view = new BrowserView();
const bounds = { x: 10, y: 20, width: 30, height: 40 };
view.setBounds(bounds);
expect(view.getBounds()).to.deep.equal(bounds);
w.addBrowserView(view);
expect(view.getBounds()).to.deep.equal(bounds);
});
});
describe('BrowserWindow.setBrowserView()', () => {

View File

@@ -283,6 +283,29 @@ describe('setDisplayMediaRequestHandler', () => {
expect(ok).to.be.true(message);
});
it('returns a MediaStream with BrowserCaptureMediaStreamTrack when the current tab is selected', async () => {
const ses = session.fromPartition('' + Math.random());
let requestHandlerCalled = false;
ses.setDisplayMediaRequestHandler((request, callback) => {
requestHandlerCalled = true;
callback({ video: w.webContents.mainFrame });
});
const w = new BrowserWindow({ show: false, webPreferences: { session: ses } });
await w.loadURL(serverUrl);
const { ok, message } = await w.webContents.executeJavaScript(`
navigator.mediaDevices.getDisplayMedia({
preferCurrentTab: true,
video: true,
audio: false,
}).then(stream => {
const [videoTrack] = stream.getVideoTracks();
return { ok: videoTrack instanceof BrowserCaptureMediaStreamTrack, message: null };
}, e => ({ok: false, message: e.message}))
`, true);
expect(requestHandlerCalled).to.be.true();
expect(ok).to.be.true(message);
});
ifit(!(process.platform === 'darwin' && process.arch === 'x64'))('can supply a screen response to preferCurrentTab', async () => {
const ses = session.fromPartition('' + Math.random());
let requestHandlerCalled = false;

View File

@@ -827,277 +827,383 @@ describe('session module', () => {
fs.unlinkSync(downloadFilePath);
};
it('can download using session.downloadURL', (done) => {
session.defaultSession.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
assertDownload(state, item);
done();
} catch (e) {
done(e);
}
});
});
session.defaultSession.downloadURL(`${url}:${port}`);
});
it('can download using session.downloadURL with a valid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
session.defaultSession.once('will-download', (e, item) => {
item.savePath = downloadFilePath;
item.on('done', () => {
try {
resolve(item);
} catch {}
});
});
});
session.defaultSession.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'Basic i-am-an-auth-header'
}
});
const item = await downloadDone;
expect(item.getState()).to.equal('completed');
expect(item.getFilename()).to.equal('mock.pdf');
expect(item.getMimeType()).to.equal('application/pdf');
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
expect(item.getTotalBytes()).to.equal(mockPDF.length);
expect(item.getContentDisposition()).to.equal(contentDisposition);
});
it('throws when session.downloadURL is called with invalid headers', () => {
expect(() => {
session.defaultSession.downloadURL(`${url}:${port}`, {
// @ts-ignore this line is intentionally incorrect
headers: 'i-am-a-bad-header'
});
}).to.throw(/Invalid value for headers - must be an object/);
});
it('can download using session.downloadURL with an invalid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
session.defaultSession.once('will-download', (_, item) => {
item.savePath = downloadFilePath;
item.on('done', (e, state) => {
console.log(state);
try {
resolve(item);
} catch {}
});
});
});
session.defaultSession.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'wtf-is-this'
}
});
const item = await downloadFailed;
expect(item.getState()).to.equal('interrupted');
expect(item.getReceivedBytes()).to.equal(0);
expect(item.getTotalBytes()).to.equal(0);
});
it('can download using WebContents.downloadURL', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
assertDownload(state, item);
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`${url}:${port}`);
});
it('can download from custom protocols using WebContents.downloadURL', (done) => {
const protocol = session.defaultSession.protocol;
const handler = (ignoredError: any, callback: Function) => {
callback({ url: `${url}:${port}` });
};
protocol.registerHttpProtocol(protocolName, handler);
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
assertDownload(state, item, true);
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`${protocolName}://item`);
});
it('can download using WebView.downloadURL', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
await w.loadURL('about:blank');
function webviewDownload ({ fixtures, url, port }: {fixtures: string, url: string, port: string}) {
const webview = new (window as any).WebView();
webview.addEventListener('did-finish-load', () => {
webview.downloadURL(`${url}:${port}/`);
});
webview.src = `file://${fixtures}/api/blank.html`;
document.body.appendChild(webview);
}
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
describe('session.downloadURL', () => {
it('can perform a download', (done) => {
session.defaultSession.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
resolve([state, item]);
});
});
});
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
const [state, item] = await done;
assertDownload(state, item);
});
it('can cancel download', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
expect(state).to.equal('cancelled');
expect(item.getFilename()).to.equal('mock.pdf');
expect(item.getMimeType()).to.equal('application/pdf');
expect(item.getReceivedBytes()).to.equal(0);
expect(item.getTotalBytes()).to.equal(mockPDF.length);
expect(item.getContentDisposition()).to.equal(contentDisposition);
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}/`);
});
it('can generate a default filename', function (done) {
if (process.env.APPVEYOR === 'True') {
// FIXME(alexeykuzmin): Skip the test.
// this.skip()
return done();
}
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function () {
try {
expect(item.getFilename()).to.equal('download.pdf');
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
});
it('can set options for the save dialog', (done) => {
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
const options = {
window: null,
title: 'title',
message: 'message',
buttonLabel: 'buttonLabel',
nameFieldLabel: 'nameFieldLabel',
defaultPath: '/',
filters: [{
name: '1', extensions: ['.1', '.2']
}, {
name: '2', extensions: ['.3', '.4', '.5']
}],
showsTagField: true,
securityScopedBookmarks: true
};
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(filePath);
item.setSaveDialogOptions(options);
item.on('done', function () {
try {
expect(item.getSaveDialogOptions()).to.deep.equal(options);
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}`);
});
describe('when a save path is specified and the URL is unavailable', () => {
it('does not display a save dialog and reports the done state as interrupted', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
if (item.getState() === 'interrupted') {
item.resume();
}
item.on('done', function (e, state) {
try {
expect(state).to.equal('interrupted');
assertDownload(state, item);
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
session.defaultSession.downloadURL(`${url}:${port}`);
});
it('can perform a download with a valid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
session.defaultSession.once('will-download', (e, item) => {
item.savePath = downloadFilePath;
item.on('done', () => {
try {
resolve(item);
} catch { }
});
});
});
session.defaultSession.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'Basic i-am-an-auth-header'
}
});
const item = await downloadDone;
expect(item.getState()).to.equal('completed');
expect(item.getFilename()).to.equal('mock.pdf');
expect(item.getMimeType()).to.equal('application/pdf');
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
expect(item.getTotalBytes()).to.equal(mockPDF.length);
expect(item.getContentDisposition()).to.equal(contentDisposition);
});
it('throws when called with invalid headers', () => {
expect(() => {
session.defaultSession.downloadURL(`${url}:${port}`, {
// @ts-ignore this line is intentionally incorrect
headers: 'i-am-a-bad-header'
});
}).to.throw(/Invalid value for headers - must be an object/);
});
it('correctly handles a download with an invalid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
session.defaultSession.once('will-download', (_, item) => {
item.savePath = downloadFilePath;
item.on('done', (e, state) => {
console.log(state);
try {
resolve(item);
} catch { }
});
});
});
session.defaultSession.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'wtf-is-this'
}
});
const item = await downloadFailed;
expect(item.getState()).to.equal('interrupted');
expect(item.getReceivedBytes()).to.equal(0);
expect(item.getTotalBytes()).to.equal(0);
});
});
describe('webContents.downloadURL', () => {
it('can perform a download', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
assertDownload(state, item);
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`${url}:${port}`);
});
it('can perform a download with a valid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const w = new BrowserWindow({ show: false });
const downloadDone: Promise<Electron.DownloadItem> = new Promise((resolve) => {
w.webContents.session.once('will-download', (e, item) => {
item.savePath = downloadFilePath;
item.on('done', () => {
try {
resolve(item);
} catch { }
});
});
});
w.webContents.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'Basic i-am-an-auth-header'
}
});
const item = await downloadDone;
expect(item.getState()).to.equal('completed');
expect(item.getFilename()).to.equal('mock.pdf');
expect(item.getMimeType()).to.equal('application/pdf');
expect(item.getReceivedBytes()).to.equal(mockPDF.length);
expect(item.getTotalBytes()).to.equal(mockPDF.length);
expect(item.getContentDisposition()).to.equal(contentDisposition);
});
it('throws when called with invalid headers', () => {
const w = new BrowserWindow({ show: false });
expect(() => {
w.webContents.downloadURL(`${url}:${port}`, {
// @ts-ignore this line is intentionally incorrect
headers: 'i-am-a-bad-header'
});
}).to.throw(/Invalid value for headers - must be an object/);
});
it('correctly handles a download and an invalid auth header', async () => {
const server = http.createServer((req, res) => {
const { authorization } = req.headers;
if (!authorization || authorization !== 'Basic i-am-an-auth-header') {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Restricted"');
res.end();
} else {
res.writeHead(200, {
'Content-Length': mockPDF.length,
'Content-Type': 'application/pdf',
'Content-Disposition': req.url === '/?testFilename' ? 'inline' : contentDisposition
});
res.end(mockPDF);
}
});
const { port } = await listen(server);
const w = new BrowserWindow({ show: false });
const downloadFailed: Promise<Electron.DownloadItem> = new Promise((resolve) => {
w.webContents.session.once('will-download', (_, item) => {
item.savePath = downloadFilePath;
item.on('done', (e, state) => {
console.log(state);
try {
resolve(item);
} catch { }
});
});
});
w.webContents.downloadURL(`${url}:${port}`, {
headers: {
Authorization: 'wtf-is-this'
}
});
const item = await downloadFailed;
expect(item.getState()).to.equal('interrupted');
expect(item.getReceivedBytes()).to.equal(0);
expect(item.getTotalBytes()).to.equal(0);
});
it('can download from custom protocols', (done) => {
const protocol = session.defaultSession.protocol;
const handler = (ignoredError: any, callback: Function) => {
callback({ url: `${url}:${port}` });
};
protocol.registerHttpProtocol(protocolName, handler);
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
assertDownload(state, item, true);
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`${protocolName}://item`);
});
it('can cancel download', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
try {
expect(state).to.equal('cancelled');
expect(item.getFilename()).to.equal('mock.pdf');
expect(item.getMimeType()).to.equal('application/pdf');
expect(item.getReceivedBytes()).to.equal(0);
expect(item.getTotalBytes()).to.equal(mockPDF.length);
expect(item.getContentDisposition()).to.equal(contentDisposition);
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}/`);
});
it('can generate a default filename', function (done) {
if (process.env.APPVEYOR === 'True') {
// FIXME(alexeykuzmin): Skip the test.
// this.skip()
return done();
}
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function () {
try {
expect(item.getFilename()).to.equal('download.pdf');
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}/?testFilename`);
});
it('can set options for the save dialog', (done) => {
const filePath = path.join(__dirname, 'fixtures', 'mock.pdf');
const options = {
window: null,
title: 'title',
message: 'message',
buttonLabel: 'buttonLabel',
nameFieldLabel: 'nameFieldLabel',
defaultPath: '/',
filters: [{
name: '1', extensions: ['.1', '.2']
}, {
name: '2', extensions: ['.3', '.4', '.5']
}],
showsTagField: true,
securityScopedBookmarks: true
};
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.setSavePath(filePath);
item.setSaveDialogOptions(options);
item.on('done', function () {
try {
expect(item.getSaveDialogOptions()).to.deep.equal(options);
done();
} catch (e) {
done(e);
}
});
item.cancel();
});
w.webContents.downloadURL(`${url}:${port}`);
});
describe('when a save path is specified and the URL is unavailable', () => {
it('does not display a save dialog and reports the done state as interrupted', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
if (item.getState() === 'interrupted') {
item.resume();
}
item.on('done', function (e, state) {
try {
expect(state).to.equal('interrupted');
done();
} catch (e) {
done(e);
}
});
});
w.webContents.downloadURL(`file://${path.join(__dirname, 'does-not-exist.txt')}`);
});
});
});
describe('WebView.downloadURL', () => {
it('can perform a download', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { webviewTag: true } });
await w.loadURL('about:blank');
function webviewDownload ({ fixtures, url, port }: { fixtures: string, url: string, port: string }) {
const webview = new (window as any).WebView();
webview.addEventListener('did-finish-load', () => {
webview.downloadURL(`${url}:${port}/`);
});
webview.src = `file://${fixtures}/api/blank.html`;
document.body.appendChild(webview);
}
const done: Promise<[string, Electron.DownloadItem]> = new Promise(resolve => {
w.webContents.session.once('will-download', function (e, item) {
item.savePath = downloadFilePath;
item.on('done', function (e, state) {
resolve([state, item]);
});
});
});
await w.webContents.executeJavaScript(`(${webviewDownload})(${JSON.stringify({ fixtures, url, port })})`);
const [state, item] = await done;
assertDownload(state, item);
});
});
});

View File

@@ -2050,10 +2050,32 @@ describe('chromium features', () => {
});
});
describe('chrome://accessibility', () => {
it('loads the page successfully', async () => {
const w = new BrowserWindow({ show: false });
await w.loadURL('chrome://accessibility');
const pageExists = await w.webContents.executeJavaScript(
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
);
expect(pageExists).to.be.true();
});
});
describe('chrome://gpu', () => {
it('loads the page successfully', async () => {
const w = new BrowserWindow({ show: false });
await w.loadURL('chrome://gpu');
const pageExists = await w.webContents.executeJavaScript(
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
);
expect(pageExists).to.be.true();
});
});
describe('chrome://media-internals', () => {
it('loads the page successfully', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL('chrome://media-internals');
await w.loadURL('chrome://media-internals');
const pageExists = await w.webContents.executeJavaScript(
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
);
@@ -2064,7 +2086,7 @@ describe('chromium features', () => {
describe('chrome://webrtc-internals', () => {
it('loads the page successfully', async () => {
const w = new BrowserWindow({ show: false });
w.loadURL('chrome://webrtc-internals');
await w.loadURL('chrome://webrtc-internals');
const pageExists = await w.webContents.executeJavaScript(
"window.hasOwnProperty('chrome') && window.chrome.hasOwnProperty('send')"
);

View File

@@ -842,15 +842,14 @@ describe('chrome extensions', () => {
before(async () => {
customSession = session.fromPartition(`persist:${uuid.v4()}`);
await customSession.loadExtension(path.join(fixtures, 'extensions', 'tabs-api-async'));
await customSession.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'api-async'));
});
beforeEach(() => {
w = new BrowserWindow({
show: false,
webPreferences: {
session: customSession,
nodeIntegration: true
session: customSession
}
});
});
@@ -913,27 +912,55 @@ describe('chrome extensions', () => {
});
});
it('get', async () => {
await w.loadURL(url);
describe('get', () => {
it('returns tab properties', async () => {
await w.loadURL(url);
const message = { method: 'get' };
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
const message = { method: 'get' };
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
const [,, responseString] = await once(w.webContents, 'console-message');
const [,, responseString] = await once(w.webContents, 'console-message');
const response = JSON.parse(responseString);
expect(response).to.have.property('active').that.is.a('boolean');
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
expect(response).to.have.property('discarded').that.is.a('boolean');
expect(response).to.have.property('groupId').that.is.a('number');
expect(response).to.have.property('highlighted').that.is.a('boolean');
expect(response).to.have.property('id').that.is.a('number');
expect(response).to.have.property('incognito').that.is.a('boolean');
expect(response).to.have.property('index').that.is.a('number');
expect(response).to.have.property('pinned').that.is.a('boolean');
expect(response).to.have.property('selected').that.is.a('boolean');
expect(response).to.have.property('url').that.is.a('string');
expect(response).to.have.property('windowId').that.is.a('number');
const response = JSON.parse(responseString);
expect(response).to.have.property('url').that.is.a('string');
expect(response).to.have.property('title').that.is.a('string');
expect(response).to.have.property('active').that.is.a('boolean');
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
expect(response).to.have.property('discarded').that.is.a('boolean');
expect(response).to.have.property('groupId').that.is.a('number');
expect(response).to.have.property('highlighted').that.is.a('boolean');
expect(response).to.have.property('id').that.is.a('number');
expect(response).to.have.property('incognito').that.is.a('boolean');
expect(response).to.have.property('index').that.is.a('number');
expect(response).to.have.property('pinned').that.is.a('boolean');
expect(response).to.have.property('selected').that.is.a('boolean');
expect(response).to.have.property('windowId').that.is.a('number');
});
it('does not return privileged properties without tabs permission', async () => {
const noPrivilegeSes = session.fromPartition(`persist:${uuid.v4()}`);
await noPrivilegeSes.loadExtension(path.join(fixtures, 'extensions', 'chrome-tabs', 'no-privileges'));
w = new BrowserWindow({ show: false, webPreferences: { session: noPrivilegeSes } });
await w.loadURL(url);
w.webContents.executeJavaScript('window.postMessage(\'{}\', \'*\')');
const [,, responseString] = await once(w.webContents, 'console-message');
const response = JSON.parse(responseString);
expect(response).not.to.have.property('url');
expect(response).not.to.have.property('title');
expect(response).to.have.property('active').that.is.a('boolean');
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
expect(response).to.have.property('discarded').that.is.a('boolean');
expect(response).to.have.property('groupId').that.is.a('number');
expect(response).to.have.property('highlighted').that.is.a('boolean');
expect(response).to.have.property('id').that.is.a('number');
expect(response).to.have.property('incognito').that.is.a('boolean');
expect(response).to.have.property('index').that.is.a('number');
expect(response).to.have.property('pinned').that.is.a('boolean');
expect(response).to.have.property('selected').that.is.a('boolean');
expect(response).to.have.property('windowId').that.is.a('number');
});
});
it('reload', async () => {
@@ -960,6 +987,19 @@ describe('chrome extensions', () => {
const [,, responseString] = await once(w.webContents, 'console-message');
const response = JSON.parse(responseString);
expect(response).to.have.property('url').that.is.a('string');
expect(response).to.have.property('title').that.is.a('string');
expect(response).to.have.property('active').that.is.a('boolean');
expect(response).to.have.property('autoDiscardable').that.is.a('boolean');
expect(response).to.have.property('discarded').that.is.a('boolean');
expect(response).to.have.property('groupId').that.is.a('number');
expect(response).to.have.property('highlighted').that.is.a('boolean');
expect(response).to.have.property('id').that.is.a('number');
expect(response).to.have.property('incognito').that.is.a('boolean');
expect(response).to.have.property('index').that.is.a('number');
expect(response).to.have.property('pinned').that.is.a('boolean');
expect(response).to.have.property('selected').that.is.a('boolean');
expect(response).to.have.property('windowId').that.is.a('number');
expect(response).to.have.property('mutedInfo').that.is.a('object');
const { mutedInfo } = response;
expect(mutedInfo).to.deep.eq({

View File

@@ -1,5 +1,5 @@
{
"name": "tabs-api-async",
"name": "api-async",
"version": "1.0",
"content_scripts": [
{
@@ -8,6 +8,7 @@
"run_at": "document_start"
}
],
"permissions": ["tabs"],
"background": {
"service_worker": "background.js"
},

View File

@@ -0,0 +1,6 @@
/* global chrome */
chrome.runtime.onMessage.addListener((_request, sender, sendResponse) => {
chrome.tabs.get(sender.tab.id).then(sendResponse);
return true;
});

View File

@@ -0,0 +1,11 @@
/* global chrome */
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
sendResponse(request);
});
window.addEventListener('message', () => {
chrome.runtime.sendMessage({}, response => {
console.log(JSON.stringify(response));
});
}, false);

View File

@@ -0,0 +1,19 @@
{
"name": "no-privileges",
"version": "1.0",
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"main.js"
],
"run_at": "document_start"
}
],
"background": {
"service_worker": "background.js"
},
"manifest_version": 3
}