Skip to content

Commit c408c21

Browse files
committed
feat: screenshot api and tests
1 parent c7b47e1 commit c408c21

3 files changed

Lines changed: 214 additions & 1 deletion

File tree

src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<meta charset="utf-8">
2828
<meta http-equiv="Content-Security-Policy"
2929
content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: asset: https://asset.localhost localhost:* ws://localhost:* ws://127.0.0.1:* https://storage.googleapis.com https://platform.twitter.com https://buttons.github.io https://unpkg.com/@aicore/ https://www.googletagmanager.com;
30-
img-src * data: localhost:* asset: https://asset.localhost ;
30+
img-src * data: blob: localhost:* asset: https://asset.localhost ;
3131
media-src * data: localhost:* asset: https://asset.localhost ;
3232
font-src * data: localhost:* asset: https://asset.localhost ;
3333
frame-src * localhost:* asset: https://asset.localhost ;

src/phoenix/shell.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,41 @@ Phoenix.libs = {
147147
// global API is only usable/stable after App init
148148
Phoenix.globalAPI = {};
149149

150+
async function _capturePageBinary(rect) {
151+
if (!Phoenix.isNativeApp) {
152+
throw new Error("Screenshot capture is not supported in browsers");
153+
}
154+
if (rect !== undefined) {
155+
if (rect.x === undefined || rect.y === undefined ||
156+
rect.width === undefined || rect.height === undefined) {
157+
throw new Error("rect must include all fields: x, y, width, height");
158+
}
159+
if (typeof rect.x !== 'number' || typeof rect.y !== 'number' ||
160+
typeof rect.width !== 'number' || typeof rect.height !== 'number') {
161+
throw new Error("rect fields x, y, width, height must be numbers");
162+
}
163+
if (rect.x < 0 || rect.y < 0 || rect.width < 0 || rect.height < 0) {
164+
throw new Error("rect fields x, y, width, height must be non-negative");
165+
}
166+
if (rect.width <= 0 || rect.height <= 0) {
167+
throw new Error("rect width and height must be greater than 0");
168+
}
169+
if (rect.x + rect.width > window.innerWidth) {
170+
throw new Error("rect x + width exceeds window innerWidth");
171+
}
172+
if (rect.y + rect.height > window.innerHeight) {
173+
throw new Error("rect y + height exceeds window innerHeight");
174+
}
175+
}
176+
if (window.__TAURI__) {
177+
const bytes = await window.__TAURI__.invoke('capture_page', { rect });
178+
return new Uint8Array(bytes);
179+
}
180+
if (window.__ELECTRON__) {
181+
return window.electronAPI.capturePage(rect);
182+
}
183+
}
184+
150185
Phoenix.app = {
151186
getNodeState: function (cbfn){
152187
cbfn(new Error('Node cannot be run in phoenix browser mode'));
@@ -794,6 +829,28 @@ Phoenix.app = {
794829
return window.electronAPI.onWindowEvent(eventName, callback);
795830
}
796831
return () => {}; // No-op for unsupported platforms
832+
},
833+
screenShotBinary: function (rect) {
834+
return _capturePageBinary(rect);
835+
},
836+
screenShotToBlob: async function (rect) {
837+
const bytes = await _capturePageBinary(rect);
838+
return new Blob([bytes], { type: "image/png" });
839+
},
840+
screenShotToPNGFile: async function (filePathToSave, rect) {
841+
if (!filePathToSave || typeof filePathToSave !== 'string') {
842+
throw new Error("filePathToSave must be a non-empty string");
843+
}
844+
const bytes = await _capturePageBinary(rect);
845+
return new Promise((resolve, reject) => {
846+
fs.writeFile(filePathToSave, bytes.buffer, 'binary', (err) => {
847+
if (err) {
848+
reject(err);
849+
} else {
850+
resolve();
851+
}
852+
});
853+
});
797854
}
798855
};
799856

test/spec/Native-platform-test.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,162 @@ define(function (require, exports, module) {
390390
});
391391
});
392392

393+
describe("Screenshot Capture API Tests", function () {
394+
const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10]; // PNG magic bytes
395+
396+
function isPNG(bytes) {
397+
if (bytes.length < 8) {
398+
return false;
399+
}
400+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
401+
if (bytes[i] !== PNG_SIGNATURE[i]) {
402+
return false;
403+
}
404+
}
405+
return true;
406+
}
407+
408+
describe("screenShotBinary", function () {
409+
it("Should return a Uint8Array of PNG data for full page capture", async function () {
410+
const bytes = await Phoenix.app.screenShotBinary();
411+
expect(bytes instanceof Uint8Array).toBeTrue();
412+
expect(bytes.length).toBeGreaterThan(0);
413+
expect(isPNG(bytes)).withContext("Result should be valid PNG data").toBeTrue();
414+
});
415+
416+
it("Should return a Uint8Array of PNG data for bounded capture", async function () {
417+
const bytes = await Phoenix.app.screenShotBinary({x: 0, y: 0, width: 100, height: 100});
418+
expect(bytes instanceof Uint8Array).toBeTrue();
419+
expect(bytes.length).toBeGreaterThan(0);
420+
expect(isPNG(bytes)).withContext("Result should be valid PNG data").toBeTrue();
421+
});
422+
423+
it("Should throw when rect is missing required fields", async function () {
424+
await expectAsync(
425+
Phoenix.app.screenShotBinary({x: 0, y: 0})
426+
).toBeRejectedWithError("rect must include all fields: x, y, width, height");
427+
});
428+
429+
it("Should throw when rect fields are not numbers", async function () {
430+
await expectAsync(
431+
Phoenix.app.screenShotBinary({x: "0", y: 0, width: 100, height: 100})
432+
).toBeRejectedWithError("rect fields x, y, width, height must be numbers");
433+
});
434+
435+
it("Should throw when rect fields are negative", async function () {
436+
await expectAsync(
437+
Phoenix.app.screenShotBinary({x: -1, y: 0, width: 100, height: 100})
438+
).toBeRejectedWithError("rect fields x, y, width, height must be non-negative");
439+
});
440+
441+
it("Should throw when rect width is 0", async function () {
442+
await expectAsync(
443+
Phoenix.app.screenShotBinary({x: 0, y: 0, width: 0, height: 100})
444+
).toBeRejectedWithError("rect width and height must be greater than 0");
445+
});
446+
447+
it("Should throw when rect height is 0", async function () {
448+
await expectAsync(
449+
Phoenix.app.screenShotBinary({x: 0, y: 0, width: 100, height: 0})
450+
).toBeRejectedWithError("rect width and height must be greater than 0");
451+
});
452+
453+
it("Should throw when rect exceeds window width bounds", async function () {
454+
await expectAsync(
455+
Phoenix.app.screenShotBinary({x: 0, y: 0, width: 999999, height: 100})
456+
).toBeRejectedWithError("rect x + width exceeds window innerWidth");
457+
});
458+
459+
it("Should throw when rect exceeds window height bounds", async function () {
460+
await expectAsync(
461+
Phoenix.app.screenShotBinary({x: 0, y: 0, width: 100, height: 999999})
462+
).toBeRejectedWithError("rect y + height exceeds window innerHeight");
463+
});
464+
});
465+
466+
describe("screenShotToBlob", function () {
467+
it("Should return a Blob of type image/png for full page capture", async function () {
468+
const blob = await Phoenix.app.screenShotToBlob();
469+
expect(blob instanceof Blob).toBeTrue();
470+
expect(blob.type).toEqual("image/png");
471+
expect(blob.size).toBeGreaterThan(0);
472+
});
473+
474+
it("Should return a Blob of type image/png for bounded capture", async function () {
475+
const blob = await Phoenix.app.screenShotToBlob({x: 0, y: 0, width: 100, height: 100});
476+
expect(blob instanceof Blob).toBeTrue();
477+
expect(blob.type).toEqual("image/png");
478+
expect(blob.size).toBeGreaterThan(0);
479+
});
480+
});
481+
482+
describe("screenShotToPNGFile", function () {
483+
let testDir;
484+
let testFilePath;
485+
const testFileName = "screenshot-test-output.png";
486+
487+
beforeEach(async function () {
488+
const appLocalData = fs.getTauriVirtualPath(await platform.appLocalDataDir());
489+
testDir = appLocalData;
490+
testFilePath = `${testDir}/${testFileName}`;
491+
});
492+
493+
afterEach(async function () {
494+
// Always clean up the test file, even if the test failed
495+
await SpecRunnerUtils.deletePathAsync(testFilePath).catch(() => {});
496+
});
497+
498+
it("Should write a valid PNG file", async function () {
499+
await Phoenix.app.screenShotToPNGFile(testFilePath);
500+
// Read back and verify PNG signature
501+
const content = await new Promise((resolve, reject) => {
502+
fs.readFile(testFilePath, 'binary', (err, data) => {
503+
if (err) {
504+
reject(err);
505+
} else {
506+
resolve(data);
507+
}
508+
});
509+
});
510+
const bytes = new Uint8Array(content);
511+
expect(isPNG(bytes)).withContext("Written file should be valid PNG").toBeTrue();
512+
});
513+
514+
it("Should write a valid PNG file with bounded rect", async function () {
515+
await Phoenix.app.screenShotToPNGFile(testFilePath, {x: 0, y: 0, width: 100, height: 100});
516+
const content = await new Promise((resolve, reject) => {
517+
fs.readFile(testFilePath, 'binary', (err, data) => {
518+
if (err) {
519+
reject(err);
520+
} else {
521+
resolve(data);
522+
}
523+
});
524+
});
525+
const bytes = new Uint8Array(content);
526+
expect(isPNG(bytes)).withContext("Written file should be valid PNG").toBeTrue();
527+
});
528+
529+
it("Should throw when filePathToSave is not provided", async function () {
530+
await expectAsync(
531+
Phoenix.app.screenShotToPNGFile()
532+
).toBeRejectedWithError("filePathToSave must be a non-empty string");
533+
});
534+
535+
it("Should throw when filePathToSave is not a string", async function () {
536+
await expectAsync(
537+
Phoenix.app.screenShotToPNGFile(123)
538+
).toBeRejectedWithError("filePathToSave must be a non-empty string");
539+
});
540+
541+
it("Should throw when filePathToSave is an empty string", async function () {
542+
await expectAsync(
543+
Phoenix.app.screenShotToPNGFile("")
544+
).toBeRejectedWithError("filePathToSave must be a non-empty string");
545+
});
546+
});
547+
});
548+
393549
describe("Credentials OTP API Tests", function () {
394550
const scopeName = "testScope";
395551
const trustRing = window.specRunnerTestKernalModeTrust;

0 commit comments

Comments
 (0)