diff --git a/.gitignore b/.gitignore index f2ffa7a2c..fc7d30529 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules .idea *.mob* install-state.gz +frontend/tests/playwright/auth-state.json \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index da9804b01..5a10cbd1c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -132,7 +132,7 @@ "cssnano": "^7.0.7", "cypress": "13.17.0", "cypress-image-snapshot": "^4.0.1", - "dotenv": "^6.2.0", + "dotenv": "^16.5.0", "esbuild-loader": "^4.3.0", "eslint": "^9.21.0", "eslint-import-resolver-typescript": "^3.8.3", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index eafd5667e..40165138d 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -5,6 +5,7 @@ import { defineConfig, devices } from '@playwright/test'; /** * See https://playwright.dev/docs/test-configuration. */ + export default defineConfig({ testDir: './tests/playwright', fullyParallel: true, diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json new file mode 100644 index 000000000..cbcc1fbac --- /dev/null +++ b/frontend/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/frontend/tests/playwright/auth-state.json b/frontend/tests/playwright/auth-state.json deleted file mode 100644 index 2f6be5310..000000000 --- a/frontend/tests/playwright/auth-state.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "cookies": [], - "origins": [ - { - "origin": "http://localhost:3333", - "localStorage": [ - { - "name": "__$session-timezone$_local__", - "value": "true" - }, - { - "name": "i18nextLng", - "value": "en" - }, - { - "name": "theme", - "value": "light" - }, - { - "name": "__$user-gettingStarted$__", - "value": "{\"steps\":[{\"title\":\"🛠️ Install OpenReplay\",\"status\":\"completed\"},{\"title\":\"🕵️ Identify Users\",\"status\":\"completed\"},{\"title\":\"🧑‍💻 Invite Team Members\",\"status\":\"completed\"},{\"title\":\"🔌 Integrations\",\"status\":\"completed\"}],\"status\":\"completed\"}" - }, - { - "name": "__$global-destinationPath$__", - "value": "/" - }, - { - "name": "___$or_spotToken$___", - "value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4ODc2MzkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0.5Xuboo2h30P6mTYHEtzoaJTBZvZGwEMs8ywookDsY0Xp0Ah9m9K-s3WF2x-M_7LCfDOp7nBFa8j9AyKz09V0oA" - }, - { - "name": "__$session-timezone$__", - "value": "{\"label\":\"UTC +02:00\",\"value\":\"UTC+02\"}" - }, - { - "name": "__$session-mouseTrail$__", - "value": "true" - }, - { - "name": "__openreplay_health_status", - "value": "1748875801944" - }, - { - "name": "__$user-siteId$__", - "value": "109" - }, - { - "name": "__or__langBannerClosed", - "value": "0" - }, - { - "name": "AuthStore", - "value": "{\"authDetails\":\"{\\\"tenants\\\":true,\\\"sso\\\":null,\\\"ssoProvider\\\":null,\\\"enforceSSO\\\":null,\\\"edition\\\":\\\"foss\\\"}\",\"__mps__\":{\"expireInTimestamp\":1748879400231}}" - }, - { - "name": "UserStore", - "value": "{\"siteId\":null,\"tenants\":[],\"jwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTYyMTkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoiZnJvbnQ6T3BlblJlcGxheSJ9.bfMw80k15BIwHkR_JQsY_DFqDJwERZcpYLOBRbcPcm2OT_WPozDal6HS8rs5YeyW0m98HRJa1ShGoMiyQhxMJA\",\"spotJwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4ODc2MzkyLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODg3NTc5MiwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0.5Xuboo2h30P6mTYHEtzoaJTBZvZGwEMs8ywookDsY0Xp0Ah9m9K-s3WF2x-M_7LCfDOp7nBFa8j9AyKz09V0oA\",\"scopeState\":2,\"onboarding\":false,\"account\":\"{\\\"id\\\":58,\\\"email\\\":\\\"andrei@openreplay.com\\\",\\\"smtp\\\":false,\\\"expirationDate\\\":-1,\\\"permissions\\\":[],\\\"settings\\\":{\\\"modules\\\":[\\\"usability-tests\\\",\\\"feature-flags\\\"]},\\\"iceServers\\\":[],\\\"hasPassword\\\":true,\\\"apiKey\\\":\\\"48Vph82zUEWHmfPSUbgG\\\",\\\"edition\\\":\\\"foss\\\",\\\"optOut\\\":false,\\\"versionNumber\\\":\\\"1.17.0\\\",\\\"name\\\":\\\"Andrei\\\",\\\"createdAt\\\":1652690354756,\\\"admin\\\":true,\\\"superAdmin\\\":false}\"}" - }, - { - "name": "__openreplay_health_response", - "value": "{\"overallHealth\":true,\"healthMap\":{\"databases\":{\"name\":\"Databases\",\"healthOk\":true,\"subservices\":{\"postgres\":{\"health\":true,\"details\":{}}},\"serviceName\":\"databases\"},\"ingestionPipeline\":{\"name\":\"Ingestion Pipeline\",\"healthOk\":true,\"subservices\":{\"redis\":{\"health\":true,\"details\":{}}},\"serviceName\":\"ingestionPipeline\"},\"backendServices\":{\"name\":\"Backend Services\",\"healthOk\":true,\"subservices\":{\"alerts\":{\"health\":true,\"details\":{}},\"assets\":{\"health\":true,\"details\":{}},\"assist\":{\"health\":true,\"details\":{}},\"chalice\":{\"health\":true,\"details\":{}},\"db\":{\"health\":true,\"details\":{}},\"ender\":{\"health\":true,\"details\":{}},\"frontend\":{\"health\":true,\"details\":{}},\"heuristics\":{\"health\":true,\"details\":{}},\"http\":{\"health\":true,\"details\":{}},\"ingress-nginx\":{\"health\":true,\"details\":{}},\"integrations\":{\"health\":true,\"details\":{}},\"sink\":{\"health\":true,\"details\":{}},\"sourcemapreader\":{\"health\":true,\"details\":{}},\"storage\":{\"health\":true,\"details\":{}}},\"serviceName\":\"backendServices\"}},\"details\":{\"numberOfSessionsCaptured\":216638,\"numberOfEventCaptured\":1840149}}" - }, - { - "name": "__$session-filter$__", - "value": "{\"name\":\"\",\"events\":[],\"custom\":{},\"rangeValue\":\"LAST_24_HOURS\",\"startDate\":1748790000000,\"endDate\":1748876400000,\"groupByUser\":false,\"sort\":\"startTs\",\"order\":\"desc\",\"strict\":false,\"eventsOrder\":\"then\",\"limit\":10,\"page\":1,\"perPage\":10,\"tab\":\"sessions\",\"filters\":[]}" - } - ] - } - ] -} \ No newline at end of file diff --git a/frontend/tests/playwright/auth.setup.ts b/frontend/tests/playwright/auth.setup.ts index 4c4cf8a44..10fa019ed 100644 --- a/frontend/tests/playwright/auth.setup.ts +++ b/frontend/tests/playwright/auth.setup.ts @@ -1,5 +1,7 @@ import { authStateFile, testUseAuthState } from './helpers'; import { expect, test as setup } from '@playwright/test'; +import * as dotenv from 'dotenv'; +dotenv.config(); testUseAuthState(); @@ -8,6 +10,9 @@ setup.beforeEach(async ({ page }) => { }); setup('authenticate', async ({ page }) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; + await page.goto('/login'); try { @@ -15,14 +20,9 @@ setup('authenticate', async ({ page }) => { console.log('Current URL:', url); if (url.includes('login')) { - console.log('Already on login page, skipping authentication'); - await page.locator('[data-test-id="login"]').click(); - await page.locator('.ant-input-affix-wrapper').first().click(); - await page - .locator('[data-test-id="login"]') - .fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').click(); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + console.log('On login page, authenticating...'); + await page.locator('[data-test-id="login"]').fill(LOGIN); + await page.locator('[data-test-id="password"]').fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); } await expect(page.getByRole('heading', { name: 'Sessions' })).toBeVisible(); diff --git a/frontend/tests/playwright/dashboards.spec.ts b/frontend/tests/playwright/dashboards.spec.ts index ecdfe0656..21999b50d 100644 --- a/frontend/tests/playwright/dashboards.spec.ts +++ b/frontend/tests/playwright/dashboards.spec.ts @@ -1,9 +1,13 @@ import { test, expect } from '@playwright/test'; test('Check if dashboards exist', async ({ page }) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; await page.goto('http://localhost:3333/login'); - await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + await page + .locator('[data-test-id="login"]') + .fill(LOGIN); + await page.locator('[data-test-id="password"]').fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); await page.getByText('Dashboards').click(); await page.getByText('Renamed One').click(); diff --git a/frontend/tests/playwright/sessionList.spec.ts b/frontend/tests/playwright/sessionList.spec.ts index 635d7c296..a16c63098 100644 --- a/frontend/tests/playwright/sessionList.spec.ts +++ b/frontend/tests/playwright/sessionList.spec.ts @@ -1,16 +1,22 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; import { testUseAuthState } from './helpers'; testUseAuthState(); test('check session list after change period', async ({ page }) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; await page.goto('http://localhost:3333/login'); - await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + await page.locator('[data-test-id="login"]').fill(LOGIN); + await page + .locator('[data-test-id="password"]') + .fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); await page.getByRole('button', { name: 'Android caret-down' }).click(); - await page.getByRole('menuitem', { name: 'OpenReplay Documentation Site' }).click(); + await page + .getByRole('menuitem', { name: 'OpenReplay Documentation Site' }) + .click(); await page.getByRole('button', { name: 'Past 24 Hours down' }).click(); await page.getByRole('menuitem', { name: 'Past 30 Days' }).click(); await page.locator('#session-item').first().click(); -}); \ No newline at end of file +}); diff --git a/frontend/tests/playwright/sign-in.spec.ts b/frontend/tests/playwright/sign-in.spec.ts index 237de64f1..f22f595ab 100644 --- a/frontend/tests/playwright/sign-in.spec.ts +++ b/frontend/tests/playwright/sign-in.spec.ts @@ -1,9 +1,13 @@ import { test, expect } from '@playwright/test'; test('Sign in flow', async ({ page }) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; await page.goto('/'); - await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + await page + .locator('[data-test-id="login"]') + .fill(LOGIN); + await page.locator('[data-test-id="password"]').fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); await expect(page.getByRole('heading', { name: 'Sessions' })).toBeVisible(); }); \ No newline at end of file diff --git a/frontend/tests/playwright/spots.spec.ts b/frontend/tests/playwright/spots.spec.ts index 72f03001d..2891a718d 100644 --- a/frontend/tests/playwright/spots.spec.ts +++ b/frontend/tests/playwright/spots.spec.ts @@ -1,9 +1,13 @@ import { test, expect } from '@playwright/test'; test('Spots should display', async ({ page }) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; await page.goto('http://localhost:3333/login'); - await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + await page + .locator('[data-test-id="login"]') + .fill(LOGIN); + await page.locator('[data-test-id="password"]').fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); await page.getByText('Spots').click(); await page.waitForTimeout(1000); diff --git a/frontend/tests/playwright/whitescreen.spec.ts b/frontend/tests/playwright/whitescreen.spec.ts index b05aa99ad..4209a95a1 100644 --- a/frontend/tests/playwright/whitescreen.spec.ts +++ b/frontend/tests/playwright/whitescreen.spec.ts @@ -1,12 +1,19 @@ import { test, expect } from '@playwright/test'; -test('The freshest session from openreplay website doesnt have white screen', async ({ page }) => { +test('The freshest session from openreplay website doesnt have white screen', async ({ + page, +}) => { + const LOGIN = process.env.TEST_FOSS_LOGIN || ''; + const PASSWORD = process.env.TEST_FOSS_PASSWORD || ''; await page.goto('http://localhost:3333/login'); - await page.locator('[data-test-id="login"]').fill('andrei@openreplay.com'); - await page.locator('[data-test-id="password"]').fill('Andrey123!'); + await page.locator('[data-test-id="login"]').fill(LOGIN); + await page.locator('[data-test-id="password"]').fill(PASSWORD); await page.locator('[data-test-id="log-button"]').click(); await page.waitForTimeout(1000); - await page.locator('[data-test-id="session-list-header"]').locator('[data-test-id="widget-select-date-range"]').click(); + await page + .locator('[data-test-id="session-list-header"]') + .locator('[data-test-id="widget-select-date-range"]') + .click(); await page.getByText('Past 30 Days').click(); await page.locator('[data-test-id="project-dropdown"]').click(); await page.getByRole('button', { name: 'Android caret-down' }).click(); @@ -17,7 +24,7 @@ test('The freshest session from openreplay website doesnt have white screen', as if (borderBlocks.length >= 2) { const secondBlock = borderBlocks[1]; const playButton = await secondBlock.$('#play-button'); - + if (playButton) { const link = await playButton.$('a'); if (link) { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bfe632beb..4bc8323f6 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7158,10 +7158,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^6.2.0": - version: 6.2.0 - resolution: "dotenv@npm:6.2.0" - checksum: 10c1/a7e8bf58954d8d46dfd5c3accb8643b5b9d2e683da66515cd0abd8ff60735d4d21e4f11debbdbc19fa6243c85fb938eb4b590d5563c53a269ade81125444a292 +"dotenv@npm:^16.5.0": + version: 16.5.0 + resolution: "dotenv@npm:16.5.0" + checksum: 10c1/95956f0c08c33c134ada7323006fd4db047294689b4a6d7714ebd2cbeb12a4888b2d70a7ac89fdd68c39bceb95f600e74eebb0671c6f31bab87ec9a221f980ba languageName: node linkType: hard @@ -12961,7 +12961,7 @@ __metadata: cssnano: "npm:^7.0.7" cypress: "npm:13.17.0" cypress-image-snapshot: "npm:^4.0.1" - dotenv: "npm:^6.2.0" + dotenv: "npm:^16.5.0" echarts: "npm:^5.6.0" esbuild-loader: "npm:^4.3.0" eslint: "npm:^9.21.0" diff --git a/tests/playwright/auth-state.json b/tests/playwright/auth-state.json deleted file mode 100644 index 62ab75dd4..000000000 --- a/tests/playwright/auth-state.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "cookies": [], - "origins": [ - { - "origin": "http://localhost:3333", - "localStorage": [ - { - "name": "__$session-timezone$_local__", - "value": "true" - }, - { - "name": "i18nextLng", - "value": "en" - }, - { - "name": "theme", - "value": "light" - }, - { - "name": "__$user-gettingStarted$__", - "value": "{\"steps\":[{\"title\":\"🛠️ Install OpenReplay\",\"status\":\"completed\"},{\"title\":\"🕵️ Identify Users\",\"status\":\"completed\"},{\"title\":\"🧑‍💻 Invite Team Members\",\"status\":\"completed\"},{\"title\":\"🔌 Integrations\",\"status\":\"completed\"}],\"status\":\"completed\"}" - }, - { - "name": "___$or_spotToken$___", - "value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTQ1MTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0._QEHvIc8ShH0PsRPtDQAo50Dc-H-Adpu8CZKXQsPF31GSLUl5SS9MV92xntRxfcloigRA1Hz2F817EF5jrgNJg" - }, - { - "name": "__$session-timezone$__", - "value": "{\"label\":\"UTC +02:00\",\"value\":\"UTC+02\"}" - }, - { - "name": "__$session-mouseTrail$__", - "value": "true" - }, - { - "name": "__openreplay_health_status", - "value": "1748944551307" - }, - { - "name": "__$user-siteId$__", - "value": "65" - }, - { - "name": "__or__langBannerClosed", - "value": "0" - }, - { - "name": "AuthStore", - "value": "{\"authDetails\":\"{\\\"tenants\\\":true,\\\"sso\\\":null,\\\"ssoProvider\\\":null,\\\"enforceSSO\\\":null,\\\"edition\\\":\\\"foss\\\"}\",\"__mps__\":{\"expireInTimestamp\":1748948149584}}" - }, - { - "name": "UserStore", - "value": "{\"siteId\":null,\"tenants\":[],\"jwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ5MDMwOTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoiZnJvbnQ6T3BlblJlcGxheSJ9.YHb2kldXFPzP2ecGoyPOo6I7_KH0BqhimOQKa1VtvSe_LTf2AzQNvKAYmsnx6-55lWX_b4wV5g4s4cdsYexOdw\",\"spotJwt\":\"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjU4LCJ0ZW5hbnRJZCI6MSwiZXhwIjoxNzQ4OTQ1MTQxLCJpc3MiOiJPcGVuUmVwbGF5LW9zcyIsImlhdCI6MTc0ODk0NDU0MSwiYXVkIjoic3BvdDpPcGVuUmVwbGF5In0._QEHvIc8ShH0PsRPtDQAo50Dc-H-Adpu8CZKXQsPF31GSLUl5SS9MV92xntRxfcloigRA1Hz2F817EF5jrgNJg\",\"scopeState\":2,\"onboarding\":false,\"account\":\"{\\\"id\\\":58,\\\"email\\\":\\\"andrei@openreplay.com\\\",\\\"smtp\\\":false,\\\"expirationDate\\\":-1,\\\"permissions\\\":[],\\\"settings\\\":{\\\"modules\\\":[\\\"usability-tests\\\",\\\"feature-flags\\\"]},\\\"iceServers\\\":[],\\\"hasPassword\\\":true,\\\"apiKey\\\":\\\"48Vph82zUEWHmfPSUbgG\\\",\\\"edition\\\":\\\"foss\\\",\\\"optOut\\\":false,\\\"versionNumber\\\":\\\"1.17.0\\\",\\\"name\\\":\\\"Andrei\\\",\\\"createdAt\\\":1652690354756,\\\"admin\\\":true,\\\"superAdmin\\\":false}\"}" - }, - { - "name": "__openreplay_health_response", - "value": "{\"overallHealth\":true,\"healthMap\":{\"databases\":{\"name\":\"Databases\",\"healthOk\":true,\"subservices\":{\"postgres\":{\"health\":true,\"details\":{}}},\"serviceName\":\"databases\"},\"ingestionPipeline\":{\"name\":\"Ingestion Pipeline\",\"healthOk\":true,\"subservices\":{\"redis\":{\"health\":true,\"details\":{}}},\"serviceName\":\"ingestionPipeline\"},\"backendServices\":{\"name\":\"Backend Services\",\"healthOk\":true,\"subservices\":{\"alerts\":{\"health\":true,\"details\":{}},\"assets\":{\"health\":true,\"details\":{}},\"assist\":{\"health\":true,\"details\":{}},\"chalice\":{\"health\":true,\"details\":{}},\"db\":{\"health\":true,\"details\":{}},\"ender\":{\"health\":true,\"details\":{}},\"frontend\":{\"health\":true,\"details\":{}},\"heuristics\":{\"health\":true,\"details\":{}},\"http\":{\"health\":true,\"details\":{}},\"ingress-nginx\":{\"health\":true,\"details\":{}},\"integrations\":{\"health\":true,\"details\":{}},\"sink\":{\"health\":true,\"details\":{}},\"sourcemapreader\":{\"health\":true,\"details\":{}},\"storage\":{\"health\":true,\"details\":{}}},\"serviceName\":\"backendServices\"}},\"details\":{\"numberOfSessionsCaptured\":216813,\"numberOfEventCaptured\":1841202}}" - }, - { - "name": "__$session-filter$__", - "value": "{\"name\":\"\",\"events\":[],\"custom\":{},\"rangeValue\":\"LAST_30_DAYS\",\"startDate\":1746352800000,\"endDate\":1748944800000,\"groupByUser\":false,\"sort\":\"startTs\",\"order\":\"desc\",\"strict\":false,\"eventsOrder\":\"then\",\"limit\":10,\"rangeName\":\"LAST_30_DAYS\",\"page\":1,\"perPage\":10,\"tab\":\"sessions\",\"filters\":[{\"type\":\"location\",\"isEvent\":true,\"value\":[\"\"],\"operator\":\"isAny\",\"source\":\"\",\"sourceOperator\":\"\",\"filters\":[]}]}" - } - ] - } - ] -} \ No newline at end of file