UI best practices: дизайн кнопок

Данные советы будут полезны тем, кто хочет создать свой pack кнопок, или разрабатывает полноценный UI Kit.

UI best practices: дизайн кнопок
5

Создание Blockchain CTF: Исследование уязвимостей смарт-контрактов — Часть 1

Автор публикации: Лукьянов Артур, младший исследователь, CyberOK

Мы в СайберОК в ходе пентестов очень любим “взламывать” разнообразные инновационные и необычные вещи. Смарт-контракты на блокчейне давно появились на наших радарах, так как они не только предлагают прозрачность, надежность и автоматизацию, но и легко могут стать объектом…

1

Playwright. Фреймворк для быстрого E2E-тестирования

Playwright - фреймворк для E2E-тестирования приложений, он хорошо интегрируется с любыми приложениями и сайтами, предлагает большой ассортимент для кастомизации.

Установка

3

Кнопки моментального заказа товаров Amazon Dash Buttons стали доступны в трёх странах Европы

Компания Amazon запустила проект Dash Buttons (физические кнопки для моментального заказа закончившихся продуктов) в трёх странах Европы: Великобритании, Германии и Австрии. До этого воспользоваться сервисом могли только клиенты Amazon, проживающие на территории США. Об этом сообщает издание The Verge.

Кнопки моментального заказа товаров Amazon Dash Buttons стали доступны в трёх странах Европы
\n","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"e21f31b4-ed34-50fb-9be6-a2d9cfc28d2c","width":418,"height":170,"size":9284,"type":"jpg","color":"4a4a4c","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQEBAQEBAQEBAQECAgMCAgICAgQDAwIDBQQFBQUEBAQFBgcGBQUHBgQEBgkGBwgICAgIBQYJCgkICgcICAj/2wBDAQEBAQICAgQCAgQIBQQFCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAj/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABQcK/8QAIxAAAQMDAgcAAAAAAAAAAAAAAgEEBQADBhEhByIxQVJhwf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDcy14L4gLyLlClc1knDUyuWjcTjm6JKpIXMinoW6d/adFWq/R0RtGtNPD7SNB//9k="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Остаётся подключить кнопки к контракту. Например, так (для Metamask):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"const testnetServerAddress = 'https://your-testnet-rpc-url'; // Replace with your testnet server address\n\n// Function to connect MetaMask\nasync function connectMetaMask() {\n // Check if MetaMask is installed\n if (typeof window.ethereum !== 'undefined') {\n try {\n // Request MetaMask to connect\n await window.ethereum.request({ method: 'eth_requestAccounts' });\n console.log('Connected to MetaMask');\n } catch (error) {\n console.error(error);\n alert('Failed to connect to MetaMask');\n }\n } else {\n alert('MetaMask is not installed');\n }\n}\n\n// Function to invoke getSecret() function\nasync function invokeGetSecret() {\n // Check if MetaMask is connected\n if (typeof window.ethereum !== 'undefined') {\n try {\n // Get the current selected account\n const accounts = await window.ethereum.request({ method: 'eth_accounts' });\n\n // Get the contract instance\n const contract = new window.ethereum.Contract(contractAbi, contractAddress);\n\n // Call the getSecret() function\n const secret = await contract.methods.getSecret().call({ from: accounts[0] });\n console.log('Secret:', secret);\n } catch (error) {\n console.error(error);\n alert('Failed to invoke getSecret()');\n }\n } else {\n alert('Please connect to MetaMask');\n }\n}\n\n// Function to invoke changeOwner() function\nasync function invokeChangeOwner() {\n // Check if MetaMask is connected\n if (typeof window.ethereum !== 'undefined') {\n try {\n // Get the current selected account\n const accounts = await window.ethereum.request({ method: 'eth_accounts' });\n\n // Get the contract instance\n const contract = new window.ethereum.Contract(contractAbi, contractAddress);\n\n // Call the changeOwner() function\n await contract.methods.changeOwner().send({ from: accounts[0] });\n console.log('Owner changed successfully');\n } catch (error) {\n console.error(error);\n alert('Failed to invoke changeOwner()');\n }\n } else {\n alert('Please connect to MetaMask');\n }\n}\n\n// Event listener for the 'Get Secret' button\ndocument.querySelector('#button-getsecret').addEventListener('click', invokeGetSecret);\n\n// Event listener for the 'Change Owner' button\ndocument.querySelector('#button-changeowner').addEventListener('click', invokeChangeOwner);","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Игровой процесс"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Task-based часть Blockchain CTF предлагает интересный игровой процесс для участников. Они получают задачу, которая представляет собой уязвимый смарт-контракт, и их задача состоит в том, чтобы найти и эксплуатировать уязвимость для получения доступа к защищенным ресурсам или выполнения определенного действия. Лучше подготовить несколько разных “испытаний” — так участники смогут проверить свои навыки в разных областях. Такой игровой процесс стимулирует участников к активному обучению и исследованию, а также позволяет им применить свои навыки на практике — “гугление” во время CTF не только развивает чуйку и эрудицию, но и совершенствует навыки.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Подсчет баллов и объявление победителей"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После завершения Blockchain CTF процесса необходимо подсчитать баллы участников и объявить победителей. В зависимости от сложности и успешности выполнения задачи участники получают определенное количество баллов. Интересный вариант подсчёта баллов — выдача участнику монет на тестнете за выполнение заданий. Тогда побеждает самый “богатый” на конец соревнования участник.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Так, когда мы проводили CTF на Standoff 10, победителями соревнования стали: Сачивко Никита, Вячеслав Дмитриев, Греков Илья и Левчук Павел.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В следующей части статьи мы рассмотрим “Атаку на реальный смарт-контракт”, где мы расскажем, как использовали реальный контракт для CTF — реставрацию состояния смартконтракта и подготовку контракта для участников.

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":2,"reposts":0,"views":660,"hits":405,"reads":null,"online":0},"dateFavorite":0,"hitsCount":405,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/id1279951/774832-sozdanie-blockchain-ctf-issledovanie-uyazvimostei-smart-kontraktov-chast-1","author":{"id":1279951,"name":"CyberOK","nickname":null,"description":"Кибербез, открытый код, исследования www.cyberok.ru","uri":"","avatar":{"type":"image","data":{"uuid":"50913896-1580-5565-b8b9-37dfed719dd6","width":2380,"height":2380,"size":101197,"type":"png","color":"040404","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAKAAoBAREA/8QAFQABAQAAAAAAAAAAAAAAAAAABwL/xAAkEAABAwMEAQUAAAAAAAAAAAACAQMEBRESAAYHFAgTISQycf/aAAgBAQAAPwAfp/i34au7Nos5/l8PmwG3++W4oMVZzyt5GOLwF1sSuOKpdMbLdSRdBO7OI+AaVuqs0uh8qdymw6hIjw5Hejn6zAOELZ5CNluKIt09lvoCF54AJsHTEC+woSoi/qanX//Z"}},"cover":{"cover":{"type":"image","data":{"uuid":"cb0844b8-4593-5af4-bdbf-364df48f28ea","width":1280,"height":720,"size":20837,"type":"jpg","color":"040404","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/wAALCAAKAAoBAREA/8QAFgABAQEAAAAAAAAAAAAAAAAACAAJ/8QAJRAAAQIEBAcAAAAAAAAAAAAAAQMEAAIFEgYRITEIExYYM1hh/9oACAEBAAA/AMPqPgXADtyyDvHr1smupa4IoKkxby3Ki7yZHSVI6bc072HNDdo3Cd7RVEfOinZygdRR/9k="}},"cover_y":46},"achievements":[{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 25 августа 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":5607533,"userId":1279951,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5607533"},{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4168742,"userId":1279951,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4168742"}],"lastModificationDate":1764913511,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":1279951,"name":"CyberOK","nickname":null,"description":"Кибербез, открытый код, исследования www.cyberok.ru","uri":"","avatar":{"type":"image","data":{"uuid":"50913896-1580-5565-b8b9-37dfed719dd6","width":2380,"height":2380,"size":101197,"type":"png","color":"040404","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAKAAoBAREA/8QAFQABAQAAAAAAAAAAAAAAAAAABwL/xAAkEAABAwMEAQUAAAAAAAAAAAACAQMEBRESAAYHFAgTISQycf/aAAgBAQAAPwAfp/i34au7Nos5/l8PmwG3++W4oMVZzyt5GOLwF1sSuOKpdMbLdSRdBO7OI+AaVuqs0uh8qdymw6hIjw5Hejn6zAOELZ5CNluKIt09lvoCF54AJsHTEC+woSoi/qanX//Z"}},"cover":{"cover":{"type":"image","data":{"uuid":"cb0844b8-4593-5af4-bdbf-364df48f28ea","width":1280,"height":720,"size":20837,"type":"jpg","color":"040404","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/wAALCAAKAAoBAREA/8QAFgABAQEAAAAAAAAAAAAAAAAACAAJ/8QAJRAAAQIEBAcAAAAAAAAAAAAAAQMEAAIFEgYRITEIExYYM1hh/9oACAEBAAA/AMPqPgXADtyyDvHr1smupa4IoKkxby3Ki7yZHSVI6bc072HNDdo3Cd7RVEfOinZygdRR/9k="}},"cover_y":46},"achievements":[{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 25 августа 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":5607533,"userId":1279951,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5607533"},{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4168742,"userId":1279951,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4168742"}],"lastModificationDate":1764913511,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"reactions":{"counters":[{"id":1,"count":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":715790,"customUri":null,"subsiteId":1178100,"title":"Playwright. Фреймворк для быстрого E2E-тестирования","date":1685795057,"dateModified":1685795057,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

Playwright - фреймворк для E2E-тестирования приложений, он хорошо интегрируется с любыми приложениями и сайтами, предлагает большой ассортимент для кастомизации.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Установка"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Установка Playwright достаточно проста. Можно сразу же инициализировать конфигурацию и установить необходимые пакеты:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# pnpm\npnpm dlx create-playwright\n\n# npm\nnpm init playwright@latest","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если же мы не хотим устанавливать Playwright c дефолтной конфигурацией, то нам нужно:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Установить пакет Playwright","Самим добавить конфигурацию"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"pnpm i -D @playwright/test\ntouch playwright.config.js","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Конфигурация"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вся конфигурация для Playwright пишется в одном файле - playwright.config.js.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вот как выглядит базовая версия конфигурации:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n // Директория для тестов\n testDir: 'tests',\n\n // Нужно ли запускать все тесты параллельно\n fullyParallel: true,\n\n // Данная опция положит все тесты на CI, если хоть в одном тесте установлен test.only\n forbidOnly: !!process.env.CI,\n\n // Сколько будет попыток повторить тест, если он упадет\n retries: process.env.CI ? 2 : 0,\n\n // Настройка количества параллельных воркеров для тестов\n workers: process.env.CI ? 1 : undefined,\n\n // Репортер (в каком формате будет представлена информация о прошедших/не прошедших тестах)\n reporter: 'html',\n\n // Используемые в тестах данные\n use: {\n // BaseURL в тестах. Если перейдем на page.goto('/'), то окажемся на localhost:3000\n baseURL: 'http://127.0.0.1:3000',\n\n // Собирать данные при падении, даже если тест повторяется\n trace: 'on-first-retry',\n },\n\n // Конфигурируем браузеры для проекта\n projects: [\n {\n name: 'chromium',\n use: { ...devices['Desktop Chrome'] },\n },\n ],\n\n // Запускаем devServer, перед тем как запустить тесты\n webServer: {\n command: 'npm run start',\n url: 'http://127.0.0.1:3000',\n reuseExistingServer: !process.env.CI,\n },\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

К слову, process.env.CI - равен true, только тогда, когда мы запускаем наши тесты внутри CI/CD (Gitlab CI/CD, Github Actions, Jenkins, и так далее).

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Продвинутая конфигурация"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вверху был представлен базовый файлик для конфигурации Playwright. Он хорош тем, что является достаточно минималистичным, однако, если нам нужно что-то по сложнее, то нам могут потребоваться дополнительные опции. Внизу предоставлены некоторые из них:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"export default defineConfig({\n // Директория, в которую будут идти все отчеты, артефакты, видео, скриншоты\n outputDir: 'test-results',\n\n // Файл, который выполнится до начала тестов. Это так называемый бутстрап\n globalSetup: require.resolve('./global-setup'),\n\n // То же самое что и вверху, однако данный файл выполнится после окончания тестов\n globalTeardown: require.resolve('./global-teardown'),\n\n // Таймаут для тестов 💆‍♂️\n timeout: 30000,\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Настройки вверху позволяют нам изменить входные и выходные данные для тестов, а также обозначить время, в пределах которого тест будет выполняться.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Не факт, что наши тесты будут лежать в директории test, поэтому мы явно можем указать какие тесты мы хотим обрабатывать, а какие нет, с помощью двух следующих опций:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"export default defineConfig({\n // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут игнорироваться\n testIgnore: '*test-assets',\n\n // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут выполняться\n testMatch: '*todo-tests/*.spec.ts',\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Также, нам может потребоваться изменить эмуляционные данные:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Время;","Геолокацию;","Разрешения;","ViewPort;","Тему;"],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Все это также можно сделать внутри конфигурации. Поле, которое мы уже ранее обсуждали (use) - отвечает за то, какие внутри браузера будут мета-данные:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"export default defineConfig({\n use: {\n // Эмулирует @media (prefers-colors-scheme)\n colorScheme: 'dark',\n\n // Геолокация\n geolocation: { longitude: 12.492507, latitude: 41.889938 },\n\n // Локализация\n locale: 'en-GB',\n\n // Разрешения\n permissions: ['geolocation'],\n\n // Часовой пояс\n timezoneId: 'Europe/Paris',\n\n // Viewport\n viewport: { width: 1280, height: 720 },\n },\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы также можем изменять параметры сети:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Давать разрешение на загрузку;","Добавлять кастомные заголовки к HTTP-запросам;","Использовать HTTP-Auth;","Использовать приложение оффлайн;","Использовать прокси;"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"export default defineConfig({\n use: {\n // Контроллировать разрешение на загрузку файлов\n acceptDownloads: false,\n\n // Добавить кастомные HTTP-заголовки к каждому реквесту\n extraHTTPHeaders: {\n 'X-My-Header': 'value',\n },\n\n // HTTP-Auth\n httpCredentials: {\n username: 'user',\n password: 'pass',\n },\n\n // Игнорировать ошибки HTTPS во время навигации\n ignoreHTTPSErrors: true,\n\n // Оффлайн\n offline: true,\n\n // Прокси\n proxy: {\n server: 'http://myproxy.com:3128',\n bypass: 'localhost',\n },\n },\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Также полезно оставлять артефакты, если с тестом что-то не так:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"export default defineConfig({\n use: {\n // Делать скриншот, если тест упадет\n screenshot: 'only-on-failure'\n\n // Записывать tracelist после первой неудачной попытки\n trace: 'on-first-retry',\n\n // Записывать видео после первой неудачной попытки\n video: 'on-first-retry'\n },\n});","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Установка необходимых браузеров"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы начать пользоваться Playwright необходимо еще установить нужные нам браузеры. По умолчанию Playwright поддерживает Google Chrome на Desktop, однако можно добавить в конфигурацию достаточно много устройств и браузеров.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Мы можем тестировать на:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Safari;","Edge;","Google Chrome;","Firefox;"],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы установить нужные нам браузеры мы должны обратиться к конфигурациии прописать все нужные нам устройства, как это сделано здесь:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// Конфигурируем браузеры для проекта\n projects: [\n {\n name: 'chromium',\n use: { ...devices['Desktop Chrome'] },\n },\n {\n name: 'firefox',\n use: { ...devices['Desktop Firefox'] },\n },\n {\n name: 'webkit',\n use: { ...devices['Desktop Safari'] },\n },\n {\n name: 'Mobile Chrome',\n use: { ...devices['Pixel 5'] },\n },\n {\n name: 'Mobile Safari',\n use: { ...devices['iPhone 12'] },\n },\n ],","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Далее нам достаточно просто ввести npx playwright install, playwright сам найдет и скачает нужные нам браузеры.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Тестирование"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Само тестирование заключается в том, чтобы провернуть в браузере сценарий, который наиболее вероятно выполнит пользователь.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Playwright предлагает нам управлять браузером программно. Вот как выглядит простой тест на Playwright:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import { test, expect } from '@playwright/test';\n\ntest('has title', async ({ page }) => {\n await page.goto('https://playwright.dev/');\n\n // Проверяем, чтобы страница имела в заголовке Playwright\n await expect(page).toHaveTitle(/Playwright/);\n});\n\ntest('get started link', async ({ page }) => {\n await page.goto('https://playwright.dev/');\n\n // Кликаем на ссылку\n await page.getByRole('link', { name: 'Get started' }).click();\n\n // Проверяем что в URL было слово intro\n await expect(page).toHaveURL(/.*intro/);\n});","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Нахождение элементов"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы производить базовые действия в браузере программно - нам понадобится Locator API .

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сам объект Locator может только находить элементы, все действия (такие как ховер, клик и так далее) находятся в объекте элемента.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Найти элемент с помощью Locator достаточно легко, для этого нам нужно обратиться к Pages API, которое предоставляет нам доступ к странице. Мы можем сами создать новую страницу:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"const page = await context.newPage();","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

И с помощью этой страницы уже перейти на нужную нам страницу (вспоминаем await page.goto(‘url’), или же воспользоваться объектом page, который Playwright автоматически передает в каждый тест-кейс:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// В аргументах к коллбэку мы забираем объект page\ntest('has title', async ({ page }) => {\n await page.goto('https://playwright.dev/');\n await expect(page).toHaveTitle(/Playwright/);\n});","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

С помощью page мы можем найти нужный нам элемент. Внизу перечислены несколько способов как это можно сделать:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// Находим по роли\npage.getByRole('textbox');\n\n// Находим по лэйблу\npage.getByLabel('Birth date');\n\n// Находим по тексту внутри элемента\npage.getByText('Item')\n\n// Находим по селектору\npage.locator('#area')","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Действия с элементами"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как только мы нашли нужный нам элемент - мы можем проделать с ним какие-либо действия.

"}},{"type":"quote","cover":false,"hidden":false,"anchor":"","data":{"text":"

Следует упомянуть, что нахождение элемента в Playwright является синхронной операцией. А выполнение действия с элементом - асинхронной.

","subline1":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Внизу предоставлен листинг кода, в котором показано что мы можем сделать с элементом:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// Находим элемент\nconst example = page.locator('#example');\n\n// Кликаем по нему\nawait example.click();\n\n// Активируем чекбокс\nawait example.check();\n\n// Дезактивируем чекбокс\nawait example.uncheck();\n\n// Наводим мышь на элемент\nawait example.hover();\n\n// Заполняем форму (быстро)\nawait example.fill();\n\n// Заполняем форму (медленно, эмулируя реальный ввод)\nawait example.type();\n\n// Фокусируем элемент\nawait example.focus();\n\n// Нажимаем клавишу\nawait example.press();\n\n// Отправляем файлы (Drag n' drop)\nawait example.setInputFiles();\n\n// Выбираем опции из выпадающего списка\nawait example.selectOption();","lang":""}},{"type":"quote","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если вам нужна полная документация по действиям, то ее можно найти вот тут.

","subline1":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Сравнения"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Теперь пришло время для сравнений. Следует сразу же рассказать о том что во всех E2E тестах есть два типа сравнений: промежуточное и финальное.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Промежуточное сравнение помогает понять тест-кейсу правильно ли он выполняет действия и должен ли он дожидаться определенной реакции браузера, прежде чем делать следующее действие.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Например, мы отправили форму и должны увидеть тостер (уведомление) вверху экрана. Прежде чем он появится мы точно знаем, что кнопка отправки должна исчезнуть (или стать снова активной, тут в зависимости от программного решения).

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы тест-кейс не бежал сразу проверять есть ли у нас тостер мы можем проверить предварительные условия (кнопка не должна быть видна) или дождаться покуда она видна не будет (что будет означать, что реквест ушел на сервер):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// Данная строка будет дожидаться покуда кнопка исчезнет\nawait page.locator('#button').not.isVisible();","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Такие промежуточные сравнения работают без expect, и как уже было сказано ранее - не обрывают тест, если условие ложно, они ждут покуда условие будет правиво в течение определенного времени.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Промежуточные сравнения находятся в Locator API, вот некоторые из них:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"// Видим ли элемент\nawait page.locator('#button').isVisible();\n\n// Скрыт ли элемент\nawait page.locator('#button').isHidden();\n\n// Активен ли элемент (нет ли disabled среди атрибутов)\nawait page.locator('#button').isEnabled();\n\n// Неактивен ли элемент (есть ли disabled среди атрибутов)\nawait page.locator('#button').isDisabled();\n\n// Можно ли элемент редактировать\nawait page.locator('#button').isEditable();\n\n// Активен ли чекбокс\nawait page.locator('#button').isChecked();","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Финальное сравнение используется тогда, когда нам нужно проверить состояние страницы или элемента на наличие какого-то атрибута, видимости элемента.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Финальное сравнение отличается от промежуточного тем, что заключается в expect. Хорошей практикой является делать одно финальное сравнение в одном тест-кейсе:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"expect(await page.locator('#button').isVisible()).toBe(true);","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Хорошие практики"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы писать хорошие E2E-тесты нужно следовать рекомендациям, иначе можно оказаться в ситуации, что тесты

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Работают слишком медленно;","Влияют на друг-друга;","Работают неправильно;"],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Хорошие практики: Атомарность"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Первым и пожалуй одним из самых важных советов является атомарность.

"}},{"type":"quote","cover":false,"hidden":false,"anchor":"","data":{"text":"

Атомарная ( греч. άτομος — неделимое) операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена.

","subline1":"Атомарная операция — Википедия"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Тест называется атомарным, когда он из одного состояния приложения переводит приложение в другое состояние, что-то проверяет и возвращает все на свое место.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Представим кейс, где мы переходим по ссылке и проверяем на другой странице какой-то текст. После того как мы проверили текст - мы завершаем тест-кейс. Затем начинается следующий тест-кейс. Он сразу же падает, почему? Потому что следующий тест-кейс ожидает того, что мы начнем тестирование с той же страницы, с которой начинали и предыдущий. Мы же в свою очередь забыли в прошлом тест-кейсе «убрать за собой мусор», то есть оставили приложение висеть на странице, на которую мы перешли.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы такого не допустить, в Playwright, как и во всех популярных фреймворках для тестирования существуют хуки:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["beforeEach - действие перед каждым тестом;","afterEach - действие после каждого теста;","beforeAll - действие перед всеми тестами;","afterAll - действие после всех тестов;"],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если переход со страницы на страницу занимает немного времени, то мы могли зафиксить нашу проблему следюущим образом:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"test.beforeEach(async ({page}) => {\n await page.goto('/');\n // или\n await page.reload();\n});","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Хорошие практики: Селекторы"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В Playwright рекомендуют использовать селекторы, которые вам даст codegen. Codegen по умолчанию не будет цепляться к вашим CSS-селекторам, он будет пытаться цепляться за текст на странице, так как текст меняется реже, нежели селекторы (по крайней мере так думают разработчики Playwright).

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сам codegen можно запустить с помощью команды:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"npx playwright codegen","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

На моей практике оба методы (цепляться за текст и цепляться на CSS-селекторы) показывали себя крайне ненадежно. Можно конечно цепляться за id, однако тогда у элементов будут куча ID’шников в продакшене.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Часто я привязываю специальные селекторы к атрибуту data-test, сами селекторы я заношу в отдельный файл selectors.js и с помощью них уже цепляюсь в тестах. Внизу приведен пример на Vue:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В тестах же, зацепиться за данные селекторы достаточно просто, главное чтобы такие селекторы были уникальными:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"await page.locator(`[data-test=\"`${UNIQUE_SELECTOR}`\"]`).click();","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы эти селекторы не попадали в продакшн, мы можем просто вырезать их на этапе сборки.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Хорошие практики: Минимальные проверки"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Суть минимальных проверок состоит в том, чтобы проверять только те элементы (на видимость, наличие текста и так далее), которые отражают состояние приложения. Допустим у нас отправляется форма и мы ждем покуда лоадер исчезнет. Мы проверим только элемент лоадера и не будем проверять никакие другие элементы на факт их исчезновения.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если вам сложновато с данной практикой используйте npx playwright codegen и просто выполните действие которое вам нужно. Таким образом вы просто напишите минимальный набор команд, который нужен для того чтобы проверить как приложение отреагировало на пользовательский ввод.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Вместо заключения 🌚"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если вам понравилась данная статья - то вы всегда можете перейти в мой блог, там больше схожей информации о веб-разработке.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Если у вас остались вопросы - не стесняйтесь задавать их в комментариях. Хорошего времяпрепровождения! 💁🏻‍♂

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":2,"reposts":0,"views":1592,"hits":5314,"reads":null,"online":0},"dateFavorite":0,"hitsCount":5314,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/715790-playwright-freimvork-dlya-bystrogo-e2e-testirovaniya","author":{"id":1178100,"name":"Даниил Шило","nickname":null,"description":"Frontend Engineer в Firecode","uri":"","avatar":{"type":"image","data":{"uuid":"59e8fb72-4a49-5932-af48-bdb368b827e7","width":400,"height":400,"size":28098,"type":"png","color":"dfcac6","hash":"73610f6d350e00","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"ab766887-ce9f-5181-b1e9-e877385348cb","width":5120,"height":2160,"size":561896,"type":"jpg","color":"85adba","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4265614,"userId":1178100,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4265614"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":642080,"userId":1178100,"count":0,"shareImage":"https://api.vc.ru/achievements/share/642080"}],"lastModificationDate":1764913511,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":3}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":18050,"customUri":"amazon-dash-europe","subsiteId":5723,"title":"Кнопки моментального заказа товаров Amazon Dash Buttons стали доступны в трёх странах Европы","date":1472642775,"dateModified":1472642775,"blocks":[{"type":"rawhtml","cover":false,"hidden":false,"anchor":"","data":{"raw":"

Компания Amazon запустила проект Dash Buttons (физические кнопки для моментального заказа закончившихся продуктов) в трёх странах Европы: Великобритании, Германии и Австрии. До этого воспользоваться сервисом могли только клиенты Amazon, проживающие на территории США. Об этом сообщает издание The Verge.

"}},{"type":"media","cover":true,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"https://png.cmtt.space/paper-preview-fox/a/ma/amazon-dash-europe/1c3c927e4daa-original.jpg","width":500,"height":280,"size":0,"type":"jpg","color":"","hash":"","external_service":[]}}}]}},{"type":"rawhtml","cover":false,"hidden":false,"anchor":"","data":{"raw":"

Пользователи из Германии и Австрии могут приобрести кнопки по цене в €4,99, жители Великобритании — по цене в £4,99.

Amazon представила Dash Buttons в апреле 2016 года. С помощью физических кнопок подписчики программы Amazon Prime могут в одно нажатие заказывать и оплачивать закончившиеся товары и продукты: стиральный порошок, собачий корм, предметы гигиены и так далее.

По данным Amazon, к концу лета 2016 года количество покупок с помощью Dash Buttons достигло двух заказов в минуту.

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

#новость #button #amazon #Amazon_Dash #Amazon_Prime

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":2,"favorites":4,"reposts":0,"views":0,"hits":3462,"reads":null,"online":0},"dateFavorite":0,"hitsCount":3462,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":true,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":true,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/flood/18050-amazon-dash-europe","author":{"id":5723,"name":"Дарья Хохлова","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"a7c3efeb-8466-1be6-71a9-830b18d20142","width":0,"height":0,"size":1,"type":"jpg","color":"","hash":"","external_service":[]}},"cover":null,"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5415148,"userId":5723,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5415148"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 24 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1791061,"userId":5723,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1791061"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":564353,"userId":5723,"count":0,"shareImage":"https://api.vc.ru/achievements/share/564353"}],"lastModificationDate":1764913511,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":199791,"name":"Офтоп","description":"Всё, что не поместилось в другие подсайты, но всё равно может быть интересно посетителям vc.ru.","uri":"/flood","avatar":{"type":"image","data":{"uuid":"42328f5f-b62f-541e-b4e0-2029f8489d47","width":1200,"height":1200,"size":14734,"type":"png","color":"3c3c3c","hash":"406020346c486c64","external_service":[]}},"cover":{"type":"image","data":{"uuid":"ea4f90c7-7ebb-57f7-b3ec-9d7890cad203","width":960,"height":280,"size":177,"type":"png","color":"fcdcfc","hash":"","external_service":[]}},"lastModificationDate":1612968637,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"flood","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[],"reactionId":0},"isNews":true,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}}],"ogTitle":null,"ogDescription":null,"isAnonymized":true}};