diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 692e71fd5..247218091 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -175,7 +175,7 @@ e2e-pleroma: NOTIFY_EMAIL: $E2E_ADMIN_EMAIL VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8099 + E2E_BASE_URL: http://localhost:8080 script: - npm install -g yarn@1.22.22 - yarn --frozen-lockfile diff --git a/.woodpecker/test-e2e.yaml b/.woodpecker/test-e2e.yaml index a5289bd78..c0bf103cd 100644 --- a/.woodpecker/test-e2e.yaml +++ b/.woodpecker/test-e2e.yaml @@ -30,7 +30,7 @@ steps: environment: APT_CACHE_DIR: apt-cache DEBIAN_FRONTEND: noninteractive - E2E_BASE_URL: http://localhost:8099 + E2E_BASE_URL: http://localhost:8080 FF_NETWORK_PER_BUILD: "true" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1" VITE_PROXY_ORIGIN: "http://pleroma:4000" diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index c49984684..75a4979a1 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -39,8 +39,6 @@ services: interval: 5s timeout: 3s retries: 60 - ports: - - 4000:4000 e2e: build: @@ -53,7 +51,7 @@ services: CI: "1" VITE_PROXY_TARGET: http://pleroma:4000 VITE_PROXY_ORIGIN: http://localhost:4000 - E2E_BASE_URL: http://localhost:8099 + E2E_BASE_URL: http://localhost:8080 E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin} E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin} command: ["yarn", "e2e:pw"] diff --git a/package.json b/package.json index f594d7a99..cdeefc30c 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "msw": "2.14.6", "nightwatch": "3.12.2", "oxc": "^1.0.1", - "playwright": "1.61.0", + "playwright": "1.57.0", "postcss": "8.5.6", "postcss-html": "^1.5.0", "postcss-scss": "^4.0.6", diff --git a/src/api/helpers.js b/src/api/helpers.js index 5fb11a183..a7e1b624b 100644 --- a/src/api/helpers.js +++ b/src/api/helpers.js @@ -98,31 +98,41 @@ export const promisedRequest = async ({ } } - const response = await fetch(url, options) - const data = await (async () => { - const [contentType] = response.headers - .get('content-type') - .split(';') - .map((x) => x.toLowerCase().trim()) - const contentLength = parseInt(response.headers.get('content-length')) - if (contentLength === 0) return null + let response = null + try { + response = await fetch(url, options) + const data = await (async () => { + const [contentType] = response.headers + .get('content-type') + .split(';') + .map((x) => x.toLowerCase().trim()) + const contentLength = parseInt(response.headers.get('content-length')) + if (contentLength === 0) return null - switch (contentType) { - case 'text/plain': - return await response.text() - case 'application/json': - return await response.json() - default: - return await response.bytes() + switch (contentType) { + case 'text/plain': + return await response.text() + case 'application/json': + return await response.json() + default: + return await response.bytes() + } + })() + + const { ok, status } = response + + if (ok) { + return { response, status, data } + } else { + throw new StatusCodeError( + response.status, + data, + { url, options }, + response, + ) } - })() - - const { ok, status } = response - - if (ok) { - return { response, status, data } - } else { - throw new StatusCodeError(response.status, data, { url, options }, response) + } catch (error) { + throw new Error(error, { url, options }, response) } } diff --git a/src/api/oauth.js b/src/api/oauth.js index b774b3f6c..7ecb0cc3f 100644 --- a/src/api/oauth.js +++ b/src/api/oauth.js @@ -2,6 +2,8 @@ import { reduce } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' +import { StatusCodeError } from 'src/services/errors/errors.js' + const REDIRECT_URI = `${window.location.origin}/oauth-callback` export const MASTODON_APP_VERIFY_URL = '/api/v1/apps/verify_credentials' diff --git a/src/api/public.js b/src/api/public.js index bd3f56fce..12af848d7 100644 --- a/src/api/public.js +++ b/src/api/public.js @@ -1,6 +1,10 @@ +import { concat, each, last, map } from 'lodash' + import { paramsString, promisedRequest } from './helpers.js' import { + parseAttachment, + parseChat, parseLinkHeaderPagination, parseNotification, parseSource, @@ -139,6 +143,8 @@ const MASTODON_SEARCH_2 = ({ `/api/v2/search${paramsString({ q, resolve, limit, offset, following, type, withRelationships, accountId, excludeUnreviewed })}` const MASTODON_USER_SEARCH_URL = ({ q, resolve }) => `/api/v1/accounts/search${paramsString({ q, resolve })}` +const MASTODON_STREAMING = ({ accessToken, stream }) => + `/api/v1/streaming${paramsString({ accessToken, stream })}` const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers' const PLEROMA_EMOJI_REACTIONS_URL = (id) => `/api/v1/pleroma/statuses/${id}/reactions` @@ -481,6 +487,168 @@ export const search2 = ({ export const fetchKnownDomains = ({ credentials }) => promisedRequest({ url: MASTODON_KNOWN_DOMAIN_LIST_URL, credentials }) +export const getMastodonSocketURI = ({ credentials, stream }, base) => { + return base + MASTODON_STREAMING({ accessToken: credentials, stream }) +} + +const MASTODON_STREAMING_EVENTS = new Set([ + 'update', + 'notification', + 'delete', + 'filters_changed', + 'status.update', +]) + +const PLEROMA_STREAMING_EVENTS = new Set([ + 'pleroma:chat_update', + 'pleroma:respond', +]) + +// A thin wrapper around WebSocket API that allows adding a pre-processor to it +// Uses EventTarget and a CustomEvent to proxy events +export const ProcessedWS = ({ + url, + preprocessor = handleMastoWS, + id = 'Unknown', + credentials, +}) => { + const eventTarget = new EventTarget() + const socket = new WebSocket(url) + if (!socket) throw new Error(`Failed to create socket ${id}`) + const proxy = (original, eventName, processor = (a) => a) => { + original.addEventListener(eventName, (eventData) => { + eventTarget.dispatchEvent( + new CustomEvent(eventName, { detail: processor(eventData) }), + ) + }) + } + socket.addEventListener('open', (wsEvent) => { + console.debug(`[WS][${id}] Socket connected`, wsEvent) + if (credentials) { + socket.send( + JSON.stringify({ + type: 'pleroma:authenticate', + token: credentials, + }), + ) + } + }) + socket.addEventListener('error', (wsEvent) => { + console.debug(`[WS][${id}] Socket errored`, wsEvent) + }) + socket.addEventListener('close', (wsEvent) => { + console.debug( + `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, + wsEvent, + ) + }) + // Commented code reason: very spammy, uncomment to enable message debug logging + /* + socket.addEventListener('message', (wsEvent) => { + console.debug( + `[WS][${id}] Message received`, + wsEvent + ) + }) + /**/ + + const onAuthenticated = () => { + eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) + } + + proxy(socket, 'open') + proxy(socket, 'close') + proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) + proxy(socket, 'error') + + // 1000 = Normal Closure + eventTarget.close = () => { + socket.close(1000, 'Shutting down socket') + } + eventTarget.getState = () => socket.readyState + eventTarget.subscribe = (stream, args = {}) => { + console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) + socket.send( + JSON.stringify({ + type: 'subscribe', + stream, + ...args, + }), + ) + } + eventTarget.unsubscribe = (stream, args = {}) => { + console.debug( + `[WS][${id}] Unsubscribing from stream ${stream} with args`, + args, + ) + socket.send( + JSON.stringify({ + type: 'unsubscribe', + stream, + ...args, + }), + ) + } + + return eventTarget +} + +export const handleMastoWS = ( + wsEvent, + { + onAuthenticated = () => { + /* no-op */ + }, + } = {}, +) => { + const { data } = wsEvent + if (!data) return + const parsedEvent = JSON.parse(data) + const { event, payload } = parsedEvent + if ( + MASTODON_STREAMING_EVENTS.has(event) || + PLEROMA_STREAMING_EVENTS.has(event) + ) { + // MastoBE and PleromaBE both send payload for delete as a PLAIN string + if (event === 'delete') { + return { event, id: payload } + } + const data = payload ? JSON.parse(payload) : null + if (event === 'pleroma:respond') { + if (data.type === 'pleroma:authenticate') { + if (data.result === 'success') { + console.debug('[WS] Successfully authenticated') + onAuthenticated() + } else { + console.error('[WS] Unable to authenticate:', data.error) + wsEvent.target.close() + } + } + return null + } else if (event === 'update') { + return { event, status: parseStatus(data) } + } else if (event === 'status.update') { + return { event, status: parseStatus(data) } + } else if (event === 'notification') { + return { event, notification: parseNotification(data) } + } else if (event === 'pleroma:chat_update') { + return { event, chatUpdate: parseChat(data) } + } + } else { + console.warn('Unknown event', wsEvent) + return null + } +} + +export const WSConnectionStatus = Object.freeze({ + JOINED: 1, + CLOSED: 2, + ERROR: 3, + DISABLED: 4, + STARTING: 5, + STARTING_INITIAL: 6, +}) + export const fetchScrobbles = ({ accountId, limit = 1 }) => promisedRequest({ url: PLEROMA_SCROBBLES_URL(accountId, { limit }), diff --git a/src/api/user.js b/src/api/user.js index ce41c0c21..127debdea 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,4 @@ -import { concat, last } from 'lodash' +import { concat, each, last, map } from 'lodash' import { paramsString, promisedRequest } from './helpers.js' import { fetchFriends, MASTODON_STATUS_URL } from './public.js' diff --git a/src/api/websocket.js b/src/api/websocket.js deleted file mode 100644 index 44f3d391a..000000000 --- a/src/api/websocket.js +++ /dev/null @@ -1,176 +0,0 @@ -import { paramsString, promisedRequest } from './helpers.js' - -import { - parseChat, - parseNotification, - parseStatus, -} from 'src/services/entity_normalizer/entity_normalizer.service.js' - -const MASTODON_STREAMING = ({ accessToken, stream }) => - `/api/v1/streaming${paramsString({ accessToken, stream })}` - -export const getMastodonSocketURI = ({ credentials, stream }) => { - return MASTODON_STREAMING({ accessToken: credentials, stream }) -} - -const MASTODON_STREAMING_EVENTS = new Set([ - 'update', - 'notification', - 'delete', - 'filters_changed', - 'status.update', -]) - -const PLEROMA_STREAMING_EVENTS = new Set([ - 'pleroma:chat_update', - 'pleroma:respond', -]) - -// A thin wrapper around WebSocket API that allows adding a pre-processor to it -// Uses EventTarget and a CustomEvent to proxy events -export const ProcessedWS = ({ - url, - preprocessor = handleMastoWS, - id = 'Unknown', - credentials, -}) => { - const eventTarget = new EventTarget() - const socket = new WebSocket(url) - if (!socket) throw new Error(`Failed to create socket ${id}`) - const proxy = (original, eventName, processor = (a) => a) => { - original.addEventListener(eventName, (eventData) => { - eventTarget.dispatchEvent( - new CustomEvent(eventName, { detail: processor(eventData) }), - ) - }) - } - socket.addEventListener('open', (wsEvent) => { - console.debug(`[WS][${id}] Socket connected`, wsEvent) - if (credentials) { - socket.send( - JSON.stringify({ - type: 'pleroma:authenticate', - token: credentials, - }), - ) - } - }) - socket.addEventListener('error', (wsEvent) => { - console.debug(`[WS][${id}] Socket errored`, wsEvent) - }) - socket.addEventListener('close', (wsEvent) => { - console.debug( - `[WS][${id}] Socket disconnected with code ${wsEvent.code}`, - wsEvent, - ) - }) - // Commented code reason: very spammy, uncomment to enable message debug logging - /* - socket.addEventListener('message', (wsEvent) => { - console.debug( - `[WS][${id}] Message received`, - wsEvent - ) - }) - /**/ - - const onAuthenticated = () => { - eventTarget.dispatchEvent(new CustomEvent('pleroma:authenticated')) - } - - proxy(socket, 'open') - proxy(socket, 'close') - proxy(socket, 'message', (event) => preprocessor(event, { onAuthenticated })) - proxy(socket, 'error') - - // 1000 = Normal Closure - eventTarget.close = () => { - socket.close(1000, 'Shutting down socket') - } - eventTarget.getState = () => socket.readyState - eventTarget.subscribe = (stream, args = {}) => { - console.debug(`[WS][${id}] Subscribing to stream ${stream} with args`, args) - socket.send( - JSON.stringify({ - type: 'subscribe', - stream, - ...args, - }), - ) - } - eventTarget.unsubscribe = (stream, args = {}) => { - console.debug( - `[WS][${id}] Unsubscribing from stream ${stream} with args`, - args, - ) - socket.send( - JSON.stringify({ - type: 'unsubscribe', - stream, - ...args, - }), - ) - } - - return eventTarget -} - -export const handleMastoWS = ( - wsEvent, - { - onAuthenticated = () => { - /* no-op */ - }, - } = {}, -) => { - const { data } = wsEvent - if (!data) return - const parsedEvent = JSON.parse(data) - const { event, payload } = parsedEvent - if ( - MASTODON_STREAMING_EVENTS.has(event) || - PLEROMA_STREAMING_EVENTS.has(event) - ) { - // MastoBE and PleromaBE both send payload for delete as a PLAIN string - if (event === 'delete') { - return { event, id: payload } - } - const data = payload ? JSON.parse(payload) : null - if (event === 'pleroma:respond') { - if (data.type === 'pleroma:authenticate') { - if (data.result === 'success') { - console.debug('[WS] Successfully authenticated') - onAuthenticated() - } else { - if (data.error === 'already_authenticated') { - onAuthenticated() - } else { - console.error('[WS] Unable to authenticate:', data.error) - wsEvent.target.close() - } - } - } - return null - } else if (event === 'update') { - return { event, status: parseStatus(data) } - } else if (event === 'status.update') { - return { event, status: parseStatus(data) } - } else if (event === 'notification') { - return { event, notification: parseNotification(data) } - } else if (event === 'pleroma:chat_update') { - return { event, chatUpdate: parseChat(data) } - } - } else { - console.warn('Unknown event', wsEvent) - return null - } -} - -export const WSConnectionStatus = Object.freeze({ - JOINED: 1, - CLOSED: 2, - ERROR: 3, - DISABLED: 4, - STARTING: 5, - STARTING_INITIAL: 6, -}) diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 16a03ab1d..68306acbe 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -23,7 +23,7 @@ import { getOrCreateChat, sendChatMessage, } from 'src/api/chats.js' -import { WSConnectionStatus } from 'src/api/websocket.js' +import { WSConnectionStatus } from 'src/api/public.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 54bcbd373..281f2b573 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -10,8 +10,11 @@ import { useInterfaceStore } from 'src/stores/interface' import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' -import { fetchConversation, fetchStatus } from 'src/api/public.js' -import { WSConnectionStatus } from 'src/api/websocket.js' +import { + fetchConversation, + fetchStatus, + WSConnectionStatus, +} from 'src/api/public.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/modules/api.js b/src/modules/api.js index ca1aacf05..a36e21c05 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -8,12 +8,12 @@ import { useInterfaceStore } from 'src/stores/interface.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useShoutStore } from 'src/stores/shout.js' -import { fetchTimeline } from 'src/api/public.js' import { + fetchTimeline, getMastodonSocketURI, ProcessedWS, WSConnectionStatus, -} from 'src/api/websocket.js' +} from 'src/api/public.js' import followRequestFetcher from 'src/services/follow_request_fetcher/follow_request_fetcher.service' import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js' import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js' @@ -97,8 +97,9 @@ const api = { const { state, commit, dispatch, rootState } = store const timelineData = rootState.statuses.timelines.friends + const serv = useInstanceStore().server.replace('http', 'ws') const credentials = useOAuthStore().token - const url = getMastodonSocketURI({ credentials }) + const url = getMastodonSocketURI({ credentials }, serv) state.mastoUserSocket = ProcessedWS({ url, diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index a28f31775..00dfd3712 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -13,7 +13,7 @@ function humanizeErrors(errors) { export function StatusCodeError(statusCode, body, options, response) { this.name = 'StatusCodeError' this.statusCode = statusCode - this.statusText = body.error || body + this.statusText = body.error.error || body.error this.details = JSON && JSON.stringify ? JSON.stringify(body) : body this.errorData = body.error this.message = this.statusCode + ' - ' + this.statusText diff --git a/src/stores/admin_settings.js b/src/stores/admin_settings.js index 5aa91c819..06bcab9ae 100644 --- a/src/stores/admin_settings.js +++ b/src/stores/admin_settings.js @@ -87,20 +87,22 @@ export const useAdminSettingsStore = defineStore('adminSettings', { loadAdminStuff() { getInstanceDBConfig({ credentials: useOAuthStore().token, - }) - .then(({ data: backendDbConfig }) => + }).then(({ data: backendDbConfig }) => { + if (backendDbConfig.error) { + if (backendDbConfig.error.status === 400) { + backendDbConfig.error.json().then((errorJson) => { + if (/configurable_from_database/.test(errorJson.error)) { + this.setInstanceAdminNoDbConfig() + } + }) + } + } else { this.setInstanceAdminSettings({ credentials: useOAuthStore().token, backendDbConfig, - }), - ) - .catch(({ statusCode, statusText }) => { - if (statusCode === 400) { - if (/configurable_from_database/.test(statusText)) { - this.setInstanceAdminNoDbConfig() - } - } - }) + }) + } + }) if (this.descriptions === null) { getInstanceConfigDescriptions({ credentials: useOAuthStore().token, diff --git a/src/stores/bookmark_folders.js b/src/stores/bookmark_folders.js index 713a21d00..6ffec807b 100644 --- a/src/stores/bookmark_folders.js +++ b/src/stores/bookmark_folders.js @@ -27,8 +27,8 @@ export const useBookmarkFoldersStore = defineStore('bookmarkFolders', { }, actions: { startFetching() { - this.fetcher = promiseInterval(() => { - fetchBookmarkFolders({ + promiseInterval(() => { + this.fetcher = fetchBookmarkFolders({ credentials: useOAuthStore().token, }) .then(({ data: folders }) => this.setBookmarkFolders(folders)) diff --git a/src/stores/lists.js b/src/stores/lists.js index 37471d46e..7634f1061 100644 --- a/src/stores/lists.js +++ b/src/stores/lists.js @@ -34,8 +34,8 @@ export const useListsStore = defineStore('lists', { }, actions: { startFetching() { - this.fetcher = promiseInterval(() => { - fetchLists({ + promiseInterval(() => { + this.fetcher = fetchLists({ credentials: useOAuthStore().token, }) .then(({ data: lists }) => this.setLists(lists)) diff --git a/test/e2e-playwright/playwright.config.mjs b/test/e2e-playwright/playwright.config.mjs index 51a4de21e..04747ee77 100644 --- a/test/e2e-playwright/playwright.config.mjs +++ b/test/e2e-playwright/playwright.config.mjs @@ -1,7 +1,7 @@ /* global process */ import { defineConfig, devices } from 'playwright/test' -const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8099' +const baseURL = process.env.E2E_BASE_URL || 'http://localhost:8080' export default defineConfig({ testDir: './specs', @@ -25,13 +25,12 @@ export default defineConfig({ video: 'retain-on-failure', }, webServer: { - command: 'yarn dev -- --host 0.0.0.0 --port $PORT --strictPort', + command: 'yarn dev -- --host 0.0.0.0 --port 8080 --strictPort', url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, env: { ...process.env, - PORT: process.env.PORT || '8099', VITE_PROXY_TARGET: process.env.VITE_PROXY_TARGET || 'http://localhost:4000', VITE_PROXY_ORIGIN: diff --git a/test/e2e/specs/test.js b/test/e2e/specs/test.js index ba3e757fd..f8993989b 100644 --- a/test/e2e/specs/test.js +++ b/test/e2e/specs/test.js @@ -4,7 +4,7 @@ module.exports = { 'default e2e tests': function (browser) { // automatically uses dev Server port from /config.index.js - // default: http://localhost:8099 + // default: http://localhost:8080 // see nightwatch.conf.js const devServer = browser.globals.devServerURL diff --git a/test/unit/specs/stores/oauth.spec.js b/test/unit/specs/stores/oauth.spec.js index 828175c9f..c4f4ba978 100644 --- a/test/unit/specs/stores/oauth.spec.js +++ b/test/unit/specs/stores/oauth.spec.js @@ -69,12 +69,6 @@ describe('oauth store', () => { it('should use create an app and record client id and secret', async ({ worker, }) => { - worker.use( - http.post(MASTODON_APP_URL, () => { - return HttpResponse.text('Throttled', { status: 429 }) - }), - ) - const store = useOAuthStore() worker.use(...authApis()) const app = await store.createApp() diff --git a/vite.config.js b/vite.config.js index 64859ebb1..6cbba4a53 100644 --- a/vite.config.js +++ b/vite.config.js @@ -73,7 +73,6 @@ export default defineConfig(async ({ mode, command }) => { const settings = await getLocalDevSettings() const target = settings.target || 'http://localhost:4000/' const origin = settings.origin || target - const targetSW = target.replace(/^http/, 'ws') const transformSW = getTransformSWSettings(settings) const proxy = { '/api': { @@ -81,7 +80,6 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', ws: true, - rewriteWsOrigin: true, }, '/auth': { // Mastodon password reset lives here @@ -100,21 +98,20 @@ export default defineConfig(async ({ mode, command }) => { changeOrigin: true, cookieDomainRewrite: 'localhost', }, - '/oauth': { + '/socket': { target, changeOrigin: true, cookieDomainRewrite: 'localhost', - }, - '/socket': { - target: targetSW, - changeOrigin: true, - cookieDomainRewrite: 'localhost', - rewriteWsOrigin: true, ws: true, headers: { Origin: origin, }, }, + '/oauth': { + target, + changeOrigin: true, + cookieDomainRewrite: 'localhost', + }, } const swSrc = 'src/sw.js' diff --git a/yarn.lock b/yarn.lock index b60f88b7b..b8c3e3293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7289,17 +7289,17 @@ pkg-types@^1.3.1: mlly "^1.7.4" pathe "^2.0.1" -playwright-core@1.61.0: - version "1.61.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.61.0.tgz#caf8078b2a39cd7738dc75ec11cb3b47f385c3f0" - integrity sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA== +playwright-core@1.57.0: + version "1.57.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" + integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== -playwright@1.61.0: - version "1.61.0" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.61.0.tgz#7082df3df08ffa82b11420ea5fae84a40bd16e3f" - integrity sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ== +playwright@1.57.0: + version "1.57.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" + integrity sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw== dependencies: - playwright-core "1.61.0" + playwright-core "1.57.0" optionalDependencies: fsevents "2.3.2"