Merge all forks upstream + latest spago and fix tests (#3)

* Fix compiling errors building with purs/psc-set 0.14.2

* Remove bower and pulp

* Add pacakge-lock.json

* Update playwright minimum version

* evaluation needs to be one expression/statement

* Add connect/connectOverCDP functions

* Convert FFI to more modern JS

* Add bindings for focus, fill, connect among others

* Make ready for 0.15 but drop tests

* Use milliseconds for timeouts

* update spago and esm

* fix tests

* Revert "fix tests"

This reverts commit aec92cb08e.

* fix effectfulGetter

* add package-lock.json

* update .gitignore

* update CI, aff affCall to context

---------

Co-authored-by: kamoii <>
Co-authored-by: Mark Eibes <mark.eibes@gmail.com>
Co-authored-by: phtz <spamsucks@posteo.de>
This commit is contained in:
fetsorn 2024-07-11 18:59:58 +04:00 committed by GitHub
parent 3638634ae1
commit 6d0aba0f34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1711 additions and 261 deletions

View File

@ -16,5 +16,6 @@ jobs:
run: |
PATH="$PATH:./node_modules/.bin/"
npm install
npm install spago purescript
npm install spago@next purescript
npx playwright install --with-deps chromium
spago test

2
.gitignore vendored
View File

@ -7,3 +7,5 @@
/.psc*
/.purs*
/.psa*
/.spago/
/.DS_Store

View File

@ -1,17 +0,0 @@
{
"name": "purescript-playwright",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"output"
],
"dependencies": {
"purescript-prelude": "^4.1.1",
"purescript-console": "^4.4.0",
"purescript-effect": "^2.0.1"
},
"devDependencies": {
"purescript-psci-support": "^4.0.0"
}
}

57
package-lock.json generated Normal file
View File

@ -0,0 +1,57 @@
{
"name": "purescript-playwright",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "purescript-playwright",
"version": "0.0.1",
"license": "BSD-3-Clause",
"dependencies": {
"playwright": "^1.45.1"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
"integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
"dependencies": {
"playwright-core": "1.45.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
"integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View File

@ -7,11 +7,8 @@
"test": "test"
},
"dependencies": {
"bower": "^1.8.8",
"playwright": "^1.3.0",
"pulp": "^15.0.0"
"playwright": "^1.45.1"
},
"devDependencies": {},
"scripts": {
"test": "pulp test"
},

View File

@ -1,42 +0,0 @@
let upstream =
https://github.com/purescript/package-sets/releases/download/psc-0.14.2-20210629/packages.dhall sha256:534c490bb73cae75adb5a39871142fd8db5c2d74c90509797a80b8bb0d5c3f7b
let overrides =
{ untagged-union =
{ dependencies =
[ "assert"
, "console"
, "effect"
, "foreign"
, "foreign-object"
, "literals"
, "maybe"
, "newtype"
, "psci-support"
, "tuples"
, "unsafe-coerce"
]
, repo = "https://github.com/jvliwanag/purescript-untagged-union.git"
, version = "v0.3.0"
}
}
let additions =
{ literals =
{ dependencies =
[ "assert"
, "effect"
, "console"
, "integers"
, "numbers"
, "partial"
, "psci-support"
, "unsafe-coerce"
, "typelevel-prelude"
]
, repo = "https://github.com/jvliwanag/purescript-literals.git"
, version = "7b2ae20f77c67b7e419a92fdd0dc7a09b447b18e"
}
}
in upstream ⫽ overrides ⫽ additions

View File

@ -1,29 +0,0 @@
{ name = "playwright"
, dependencies =
[ "argonaut-core"
, "console"
, "effect"
, "prelude"
, "psci-support"
, "aff-promise"
, "test-unit"
, "untagged-union"
, "node-buffer"
, "node-fs-aff"
, "undefined"
, "aff"
, "either"
, "exceptions"
, "foreign"
, "foreign-object"
, "literals"
, "maybe"
, "node-streams"
, "ordered-collections"
, "refs"
, "strings"
, "transformers"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}

1386
spago.lock Normal file

File diff suppressed because it is too large Load Diff

30
spago.yaml Normal file
View File

@ -0,0 +1,30 @@
package:
dependencies:
- aff
- aff-promise
- argonaut-core
- console
- datetime
- effect
- either
- exceptions
- foreign
- foreign-object
- literals
- maybe
- node-buffer
- node-fs
- node-streams
- ordered-collections
- prelude
- psci-support
- refs
- strings
- test-unit
- transformers
- untagged-union
- literals
name: playwright
workspace:
packageSet:
registry: 53.2.0

View File

@ -1,15 +1,21 @@
/* global exports */
exports.exposeBinding_ = function (x) {
return function (name) {
return function (cb) {
return function (opts) {
return function () {
return x.exposeBinding(name, function (info, arg) {
return cb(info)(arg)();
}, opts);
};
};
export const exposeBinding_ = x => name => cb => opts => () => {
return x.exposeBinding(
name,
function (info, arg) {
return cb(info)(arg)()
},
opts
)
}
export const onResponse = function (page) {
return function (cb) {
return function () {
page.on('response', function (response) {
cb(response)();
});
};
};
};

View File

@ -1,7 +1,10 @@
module Playwright
( launch
, connect
, connectOverCDP
, close
, contexts
, context
, isConnected
, version
, newPage
@ -9,6 +12,7 @@ module Playwright
, goBack
, goto
, addCookies
, cookies
, hover
, innerHTML
, innerText
@ -40,25 +44,45 @@ module Playwright
, setViewportSize
, title
, exposeBinding
, fill
, focus
, onResponse
, connect
, module Playwright.Data
, module Playwright.Options
)
where
import Playwright.Options
import Control.Promise (Promise, fromAff, toAffE)
import Data.String.Regex (Regex)
import Data.Unit (unit)
import Effect (Effect)
import Effect.Aff (Aff)
import Foreign (Foreign, unsafeToForeign)
import Literals.Null (Null)
import Node.Buffer (Buffer)
import Playwright.Data
import Playwright.Data (Browser, BrowserContext, BrowserType, ConsoleMessage, Dialog, Download, ElementHandle, ElementState, FileChooser, Frame, JSHandle, Keyboard, Modifier, Mouse, MouseButton, Page, Raf, Request, Response, Route, ScreenshotType, Selector(..), Selectors, URL(..), WaitUntil, Worker, alt, attached, chromium, control, detached, domcontentloaded, firefox, hidden, jpg, left, load, meta, middle, networkidle, png, raf, right, shift, visible, webkit)
import Playwright.Internal (effCall, effProp, affCall)
import Playwright.Options
import Prelude (Unit, ($))
import Undefined (undefined)
import Untagged.Castable (class Castable)
import Untagged.Union (type (|+|), UndefinedOr)
import Playwright.Types (Cookie)
foreign import onResponse :: Page -> (Response -> Effect Unit) -> Effect Unit
fill
:: forall o
. Castable o FillOptions
=> Page -> Selector -> String -> o -> Aff Unit
fill = affCall "fill" \_ -> fill
focus
:: forall o
. Castable o FocusOptions
=> Page -> Selector -> o -> Aff Unit
focus = affCall "focus" \_ -> focus
launch
:: forall o
@ -67,6 +91,35 @@ launch
launch =
affCall "launch" \_ -> launch
type WebSocketEndpoint = String
connect
:: forall o
. Castable o ConnectOptions
=> BrowserType
-> WebSocketEndpoint
-> o
-> Aff Browser
connect = affCall "connect" \_ -> connect
type ConnectOptions =
{ timeout :: UndefinedOr Number
}
connectOverCDP
:: forall o
. Castable o ConnectOverCDPOptions
=> BrowserType
-> String
-> o
-> Aff Browser
connectOverCDP =
affCall "connectOverCDP" \_ -> connectOverCDP
type ConnectOverCDPOptions =
{ timeout :: UndefinedOr Number
}
close
:: forall x
. Castable x (Browser |+| BrowserContext |+| Page)
@ -116,12 +169,15 @@ goto
goto =
affCall "goto" \_ -> goto
type Cookie =
{ name :: String
, value :: String
, url :: UndefinedOr String
}
context :: Page -> BrowserContext
context = affCall "context" \_ -> context
cookies
:: BrowserContext
-> Aff (Array Cookie)
cookies =
affCall "cookies" \_ -> cookies
addCookies
:: BrowserContext
-> Array Cookie
@ -320,7 +376,7 @@ waitForFunction
-- ^ Function to be evaluated in browser context
-> o
-> Aff JSHandle
waitForFunction x s o = waitForFunction' x s (unsafeToForeign undefined) o
waitForFunction x s o = waitForFunction' x s unit o
where
waitForFunction' = affCall "waitForFunction" \_ -> waitForFunction'

View File

@ -1,31 +1,36 @@
/* global require exports */
var P = require('playwright');
import { chromium as pwChromium, firefox as pwFirefox, webkit as pwWebkit } from 'playwright';
exports.png = "png";
exports.jpg = "jpg";
export const png = "png";
export const jpg = "jpg";
exports.chromium = P.chromium;
exports.firefox = P.firefox;
exports.webkit = P.webkit;
export const chromium = pwChromium;
export const firefox = pwFirefox;
export const webkit = pwWebkit;
exports.domcontentloaded = "domcontentloaded";
exports.load = "load";
exports.networkidle = "networkidle";
export const domcontentloaded = "domcontentloaded";
export const load = "load";
export const networkidle = "networkidle";
exports.alt = "Alt";
exports.control = "Control";
exports.meta = "Meta";
exports.shift = "Shift";
export const alt = "Alt";
export const control = "Control";
export const meta = "Meta";
export const shift = "Shift";
exports.null = null;
const _null = null;
export { _null as null };
exports.left = "left";
exports.right = "right";
exports.middle = "middle";
export const left = "left";
export const right = "right";
export const middle = "middle";
exports.attached = "attached";
exports.detached = "detached";
exports.visible = "visible";
exports.hidden = "hidden";
export const attached = "attached";
export const detached = "detached";
export const visible = "visible";
export const hidden = "hidden";
exports.raf = "raf";
export const raf = "raf";
export const strict = "Strict";
export const lax = "Lax";
export const none = "None";

View File

@ -65,5 +65,10 @@ foreign import detached :: ElementState
foreign import visible :: ElementState
foreign import hidden :: ElementState
foreign import data SameSite :: Type
foreign import strict :: SameSite
foreign import lax :: SameSite
foreign import none :: SameSite
foreign import data Raf :: Type
foreign import raf :: Raf

View File

@ -1,17 +1,10 @@
/* global exports */
exports.createReadStream_ = function (Nothing) {
return function (Just) {
return function (Download) {
return function () {
return Download.createReadStream().then(function (result) {
if (result === null) {
return Nothing;
} else {
return Just(result);
}
});
};
};
};
};
export const createReadStream_ = Nothing => Just => Download => () =>
Download.createReadStream().then(result => {
if (result === null) {
return Nothing
} else {
return Just(result)
}
})

View File

@ -1,13 +1,7 @@
/* global exports */
exports.onForeign = function (obj) {
return function (eventName) {
return function (effCallback) {
return function () {
obj.on(eventName, function (argument) {
effCallback(argument)();
});
};
};
};
};
export const onForeign = (obj) => (eventName) => (effCallback) => () => {
obj.on(eventName, (argument) => {
effCallback(argument)()
})
}

View File

@ -2,7 +2,7 @@
/**
* @param {string} property - method to call on object
* @param {number} n - number of (curried) arguments
* @param {number} argsCount - number of (curried) arguments
* @param {effectRunnerWrapper} effectRunnerWrapper - a function to overrride
* effect runner with. `toAffE` for `Aff`, `identity` for `Effect`.
*
@ -19,54 +19,33 @@
* effectfulGetter('close', 0, identity);
*/
function effectfulGetter (property, argsCount, effectRunnerWrapper) {
var args = [];
return function (object) {
function effectRunner () {
return object[property].apply(object, args);
function consume(arg, args, counter) {
const argsNew = [ ...args, arg ];
if (counter === 0) {
const [ object, ...rest ] = argsNew;
return effectRunnerWrapper(() => object[property].apply(object, rest))
} else {
return (a) => consume(a, argsNew, counter - 1)
}
}
var affectRunner = effectRunnerWrapper(effectRunner);
function chooseNext () {
return argsCount > 0 ? argsConsumer : affectRunner;
}
function argsConsumer (arg) {
if (argsCount == 0) {
return affectRunner;
} else {
args.push(arg);
argsCount--;
return chooseNext();
}
}
return chooseNext();
};
return (object) => consume(object, [], argsCount)
}
function identity (x) {
return x;
return x;
}
exports.unsafeEffCall = function (method) {
return function (argsCount) {
return effectfulGetter(method, argsCount, identity);
};
};
exports.unsafeAffCall = function (toAffE) {
return function (method) {
return function (argsCount) {
return effectfulGetter(method, argsCount, toAffE);
};
};
};
exports.effProp = function (prop) {
return function (object) {
return function () {
return object[prop];
};
};
export function unsafeEffCall(method) {
return argsCount => effectfulGetter(method, argsCount, identity);
}
export function unsafeAffCall(toAffE) {
return method => argsCount => effectfulGetter(method, argsCount, toAffE);
}
export function effProp(prop) {
return object => () => object[prop];
}

View File

@ -1,17 +1,11 @@
/* global exports */
exports.getProperties_ = function (insert) {
return function (emptyMap) {
return function (jsHandle) {
return function () {
return jsHandle.getProperties().then(function (props) {
var acc = emptyMap;
props.entries().forEach(function (pair) {
acc = insert(pair[0])(pair[1])(acc);
});
return acc;
});
};
};
};
};
export function getProperties_(insert) {
return emptyMap => jsHandle => () => jsHandle.getProperties().then(props => {
let acc = emptyMap;
props.entries().forEach(pair => {
acc = insert(pair[0])(pair[1])(acc);
});
return acc;
});
}

View File

@ -3,13 +3,27 @@ module Playwright.Options where
import Playwright.Data
import Data.String.Regex (Regex)
import Data.Time.Duration (Milliseconds(..))
import Foreign (Foreign)
import Foreign.Object (Object)
import Literals.Null (Null)
import Untagged.Union (UndefinedOr, type (|+|))
import Playwright.Types (Cookie)
type Opt a = UndefinedOr a
type FocusOptions =
{ strict :: Opt Boolean
, timeout :: Opt Number
}
type FillOptions =
{ force :: Opt Boolean
, noWaitAfter :: Opt Boolean
, strict :: Opt Boolean
, timeout :: Opt Number
}
type LaunchOptions =
{ headless :: Opt Boolean
, executablePath :: Opt String
@ -22,10 +36,17 @@ type LaunchOptions =
, handleSIGINT :: Opt Boolean
, handleSIGTERM :: Opt Boolean
, handleSIGHUP :: Opt Boolean
, timeout :: Opt Number
, timeout :: Opt Milliseconds
, env :: Opt (Object String)
, devtools :: Opt Boolean
, slowMo :: Opt Number
, storageState :: Opt { cookies :: Array Cookie }
}
type ConnectOptions =
{ headers :: Opt (Object String)
, slowMo :: Opt Number
, timeout :: Opt Number
}
type ProxyOptions =
@ -40,11 +61,11 @@ type ScreenshotOptions =
, "type" :: Opt ScreenshotType
, quality :: Opt Number
, omitBackground :: Opt Boolean
, timeout :: Opt Number
, timeout :: Opt Milliseconds
}
type GotoOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
, waitUntil :: Opt WaitUntil
, referer :: Opt String
}
@ -57,7 +78,7 @@ type NewpageOptions =
}
type GoOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
, waitUntil :: Opt WaitUntil
}
@ -68,11 +89,11 @@ type HoverOptions =
}
type InnerHTMLOptions =
{ timeout :: Opt Number
{ timeout :: Opt Milliseconds
}
type InnerTextOptions =
{ timeout :: Opt Number
{ timeout :: Opt Milliseconds
}
type KeyboardPressOptions =
@ -92,7 +113,7 @@ type ClickOptions =
, modifiers :: Opt (Array Modifier)
, force :: Opt Boolean
, noWaitAfter :: Opt Boolean
, timeout :: Opt Int
, timeout :: Opt Milliseconds
}
type MouseClickOptions =
@ -116,31 +137,31 @@ type MouseMoveOptions =
}
type WaitForNavigationOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
, url :: Opt (String |+| Regex |+| URL -> Boolean)
, waitUntil :: Opt WaitUntil
}
type WaitForRequestOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
}
type WaitForResponseOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
}
type WaitForSelectorOptions =
{ state :: Opt ElementState
, timeout :: Opt Int
, timeout :: Opt Milliseconds
}
type WaitForFunctionOptions =
{ polling :: Opt (Int |+| Raf)
, timeout :: Opt Int
, timeout :: Opt Milliseconds
}
type WaitForLoadStateOptions =
{ timeout :: Opt Int
{ timeout :: Opt Milliseconds
}
type Margin =
@ -168,5 +189,5 @@ type PdfOptions =
type SetFilesOptions =
{ noWaitAfter :: Opt Boolean
, timeout :: Opt Int
, timeout :: Opt Milliseconds
}

View File

@ -1,17 +1,10 @@
/* global exports */
exports.finished_ = function (Nothing) {
return function (Just) {
return function (Response) {
return function () {
return Response.finished().then(function (result) {
if (result === null) {
return Nothing;
} else {
return Just(result);
}
});
};
};
};
};
export const finished_ = Nothing => Just => Response => () =>
Response.finished().then((result) => {
if (result === null) {
return Nothing
} else {
return Just(result)
}
})

14
src/Playwright/Types.purs Normal file
View File

@ -0,0 +1,14 @@
module Playwright.Types where
import Playwright.Data (SameSite)
type Cookie =
{ name :: String
, value :: String
, domain :: String
, path :: String
, expires :: Number
, httpOnly :: Boolean
, secure :: Boolean
, sameSite :: SameSite
}

View File

@ -3,6 +3,7 @@ module Test.Main where
import Control.Monad.Except (runExcept)
import Data.Either (isLeft)
import Data.Maybe (Maybe(..))
import Data.Time.Duration (Milliseconds(..))
import Effect (Effect)
import Effect.Aff (launchAff_, try)
import Effect.Class (liftEffect)
@ -13,6 +14,7 @@ import Node.Encoding as Encoding
import Node.FS.Aff as FS
import Node.Stream as Stream
import Playwright
import Node.EventEmitter (on_)
import Playwright.ConsoleMessage as ConsoleMessage
import Playwright.Dialog as Dialog
import Playwright.Download as Download
@ -78,7 +80,7 @@ main = runTest do
withBrowserPage hello
\page -> do
void $ waitForSelector page (Selector "body") {}
res <- try $ waitForSelector page (Selector "nonexistent") { timeout: 100 }
res <- try $ waitForSelector page (Selector "nonexistent") { timeout: Milliseconds 100.0 }
Assert.assert "waitForSelector fails when no element" $
isLeft res
test "waitForFunction" do
@ -89,7 +91,7 @@ main = runTest do
void $ waitForFunction
page
"document.body.textContent.includes('uniqstring')"
{ timeout: 5000, polling: 100 }
{ timeout: Milliseconds 5000.0, polling: 100 }
suite "FFI" do
test "exposeBinding" do
withBrowserPage hello \page -> do
@ -151,19 +153,22 @@ main = runTest do
case mbStream of
Nothing -> Assert.assert "Unable to get stream" false
Just stream -> do
liftEffect $ Stream.onDataString stream Encoding.UTF8 $ \string -> do
liftEffect $ Stream.setEncoding stream Encoding.UTF8
liftEffect $ stream # on_ Stream.dataHStr \string -> do
Ref.write (Just string) downloadRef
void $ evaluate page
"""
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
{
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
download("hello.txt","hiiii");
}
download("hello.txt","hiiii");
"""
waitForTimeout page 100
downloadContent <- liftEffect $ Ref.read downloadRef

View File

@ -1,5 +1,5 @@
/* global exports __dirname */
exports.cwd = process.cwd();
export const cwd = process.cwd();
exports.isNull = function (sth) { return sth === null; };
export const isNull = sth => sth === null;