diff --git a/app/assets/javascripts/discourse-common/addon/config/environment.js b/app/assets/javascripts/discourse-common/addon/config/environment.js index 5208c1f7568..2b2fba3de6a 100644 --- a/app/assets/javascripts/discourse-common/addon/config/environment.js +++ b/app/assets/javascripts/discourse-common/addon/config/environment.js @@ -12,8 +12,18 @@ export function setEnvironment(e) { } } +/** + * Returns true if running in the qunit test harness + */ export function isTesting() { - return environment === "testing"; + return environment === "qunit-testing"; +} + +/** + * Returns true is RAILS_ENV=test (e.g. for system specs) + */ +export function isRailsTesting() { + return environment === "test"; } // Generally means "before we migrated to Ember CLI" diff --git a/app/assets/javascripts/discourse-i18n/src/index.js b/app/assets/javascripts/discourse-i18n/src/index.js index c36bf344a76..fe5189cc2e2 100644 --- a/app/assets/javascripts/discourse-i18n/src/index.js +++ b/app/assets/javascripts/discourse-i18n/src/index.js @@ -61,6 +61,11 @@ export class I18n { return "Verbose localization is enabled. Close the browser tab to turn it off. Reload the page to see the translation keys."; } + disableVerboseLocalizationSession() { + sessionStorage.removeItem("verbose_localization"); + return "Verbose localization disabled. Reload the page."; + } + _translate(scope, options) { options = this.prepareOptions(options); options.needsPluralization = typeof options.count === "number"; diff --git a/app/assets/javascripts/discourse/app/initializers/dev-tools.js b/app/assets/javascripts/discourse/app/initializers/dev-tools.js new file mode 100644 index 00000000000..4be9a7c3902 --- /dev/null +++ b/app/assets/javascripts/discourse/app/initializers/dev-tools.js @@ -0,0 +1,68 @@ +import { DEBUG } from "@glimmer/env"; +import { isDevelopment } from "discourse-common/config/environment"; + +const KEY = "discourse__dev_tools"; + +function parseStoredValue() { + const val = window.localStorage.getItem(KEY); + if (val === "true") { + return true; + } else if (val === "false") { + return false; + } else { + return null; + } +} + +export default { + after: ["discourse-bootstrap"], + + initialize(app) { + let defaultEnabled = false; + + if (DEBUG && isDevelopment()) { + defaultEnabled = true; + } + + function storeValue(value) { + if (value === defaultEnabled) { + window.localStorage.removeItem(KEY); + } else { + window.localStorage.setItem(KEY, value); + } + } + + window.enableDevTools = () => { + storeValue(true); + window.location.reload(); + }; + + window.disableDevTools = () => { + storeValue(false); + window.location.reload(); + }; + + if (parseStoredValue() ?? defaultEnabled) { + // eslint-disable-next-line no-console + console.log("Loading Discourse dev tools..."); + + app.deferReadiness(); + + import("discourse/static/dev-tools/entrypoint").then((devTools) => { + devTools.init(); + + // eslint-disable-next-line no-console + console.log( + "Loaded Discourse dev tools. Run `disableDevTools()` in console to disable." + ); + + app.advanceReadiness(); + }); + } else if (DEBUG && isDevelopment()) { + // eslint-disable-next-line no-console + console.log( + "Discourse dev tools are disabled. Run `enableDevTools()` in console to enable." + ); + } + }, +}; diff --git a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js index dcd0f7be676..0196cf57494 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-connectors.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-connectors.js @@ -14,6 +14,7 @@ let _connectorCache; let _rawConnectorCache; let _extraConnectorClasses = {}; let _extraConnectorComponents = {}; +let debugOutletCallback; export function resetExtraClasses() { _extraConnectorClasses = {}; @@ -214,13 +215,16 @@ export function connectorsExist(outletName) { if (!_connectorCache) { buildConnectorCache(); } - return Boolean(_connectorCache[outletName]); + return Boolean(_connectorCache[outletName] || debugOutletCallback); } export function connectorsFor(outletName) { if (!_connectorCache) { buildConnectorCache(); } + if (debugOutletCallback) { + return debugOutletCallback(outletName, _connectorCache[outletName]); + } return _connectorCache[outletName] || []; } @@ -302,3 +306,7 @@ export function deprecatedArgumentValue(deprecatedArg, options) { return deprecatedArg.value; }); } + +export function _setOutletDebugCallback(callback) { + debugOutletCallback = callback; +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/entrypoint.js b/app/assets/javascripts/discourse/app/static/dev-tools/entrypoint.js new file mode 100644 index 00000000000..0c97be96e7e --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/entrypoint.js @@ -0,0 +1,12 @@ +import "./styles.css"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { patchConnectors } from "./plugin-outlet-debug/patch"; +import Toolbar from "./toolbar"; + +export function init() { + patchConnectors(); + + withPluginApi("0.8", (api) => { + api.renderInOutlet("above-site-header", Toolbar); + }); +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/args-table.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/args-table.gjs new file mode 100644 index 00000000000..03168b2dff9 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/args-table.gjs @@ -0,0 +1,69 @@ +import Component from "@glimmer/component"; +import { fn } from "@ember/helper"; +import { on } from "@ember/modifier"; +import icon from "discourse-common/helpers/d-icon"; + +let globalI = 1; + +function stringifyValue(value) { + if (value === undefined) { + return "undefined"; + } else if (value === null) { + return "null"; + } else if (["string", "number"].includes(typeof value)) { + return JSON.stringify(value); + } else if (typeof value === "boolean") { + return value.toString(); + } else if (Array.isArray(value)) { + return `Array (${value.length} items)`; + } else if (value.toString().startsWith("class ")) { + return `class ${value.name} {}`; + } else if (value.constructor.name === "function") { + return `ƒ ${value.name || "function"}(...)`; + } else if (value.id) { + return `${value.constructor.name} { id: ${value.id} }`; + } else { + return `${value.constructor.name} {}`; + } +} + +export default class ArgsTable extends Component { + get renderArgs() { + return Object.entries(this.args.outletArgs).map(([key, value]) => { + return { + key, + value: stringifyValue(value), + originalValue: value, + }; + }); + } + + writeToConsole(key, value, event) { + event.preventDefault(); + window[`arg${globalI}`] = value; + /* eslint-disable no-console */ + console.log( + `[plugin outlet debug] \`@outletArgs.${key}\` saved to global \`arg${globalI}\`, and logged below:` + ); + console.log(value); + /* eslint-enable no-console */ + + globalI++; + } + + +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/button.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/button.gjs new file mode 100644 index 00000000000..54ee5ff477b --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/button.gjs @@ -0,0 +1,26 @@ +import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse-common/helpers/d-icon"; +import devToolsState from "../state"; + +export default class PluginOutletDebugButton extends Component { + @action + togglePluginOutlets() { + devToolsState.pluginOutletDebug = !devToolsState.pluginOutletDebug; + } + + +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/outlet-info.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/outlet-info.gjs new file mode 100644 index 00000000000..642ab4cb545 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/outlet-info.gjs @@ -0,0 +1,136 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { array, hash } from "@ember/helper"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse-common/helpers/d-icon"; +import DTooltip from "float-kit/components/d-tooltip"; +import devToolsState from "../state"; +import ArgsTable from "./args-table"; + +// Outlets matching these patterns will be displayed with an icon only. +// Feel free to add more if it improves the layout. +const SMALL_OUTLETS = [ + /^topic-list-/, + "before-topic-list-body", + "after-topic-status", + /^header-contents/, + "after-header-panel", + /^bread-crumbs/, + /^user-dropdown-notifications/, + /^user-dropdown-button/, + "after-breadcrumbs", +]; + +export default class OutletInfoComponent extends Component { + static shouldRender() { + return devToolsState.pluginOutletDebug; + } + + @tracked partOfWrapper; + + get isBeforeOrAfter() { + return this.isBefore || this.isAfter; + } + + get isBefore() { + return this.args.outletName.includes("__before"); + } + + get isAfter() { + return this.args.outletName.includes("__after"); + } + + get baseName() { + return this.args.outletName.split("__")[0]; + } + + get displayName() { + return this.partOfWrapper ? this.baseName : this.args.outletName; + } + + @action + checkIsWrapper(element) { + const parent = element.parentElement; + this.partOfWrapper = [ + this.baseName, + `${this.baseName}__before`, + `${this.baseName}__after`, + ].every((name) => + parent.querySelector(`:scope > [data-outlet-name="${name}"]`) + ); + } + + get isWrapper() { + return this.partOfWrapper && !this.isBeforeOrAfter; + } + + get isHidden() { + return this.isWrapper && !this.isBeforeOrAfter; + } + + get showName() { + return !SMALL_OUTLETS.some((pattern) => + pattern.test ? pattern.test(this.baseName) : pattern === this.baseName + ); + } + + +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/patch.js b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/patch.js new file mode 100644 index 00000000000..f052b5df4c6 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/plugin-outlet-debug/patch.js @@ -0,0 +1,31 @@ +import curryComponent from "ember-curry-component"; +import { _setOutletDebugCallback } from "discourse/lib/plugin-connectors"; +import { getOwnerWithFallback } from "discourse-common/lib/get-owner"; +import devToolsState from "../state"; +import OutletInfoComponent from "./outlet-info"; + +const SKIP_EXISTING_FOR_OUTLETS = [ + "home-logo-wrapper", // Wrapper outlet used by chat, so very likely to be present +]; + +export function patchConnectors() { + _setOutletDebugCallback((outletName, existing) => { + existing ||= []; + + if (!devToolsState.pluginOutletDebug) { + return existing; + } + + if (SKIP_EXISTING_FOR_OUTLETS.includes(outletName)) { + existing = []; + } + + const componentClass = curryComponent( + OutletInfoComponent, + { outletName }, + getOwnerWithFallback() + ); + + return [{ componentClass }, ...existing]; + }); +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/safe-mode/button.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/safe-mode/button.gjs new file mode 100644 index 00000000000..1c02e90f8ce --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/safe-mode/button.gjs @@ -0,0 +1,35 @@ +import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse-common/helpers/d-icon"; + +export default class PluginOutletDebugButton extends Component { + get safeModeActive() { + return new URLSearchParams(window.location.search).has("safe_mode"); + } + + @action + toggleSafeMode() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has("safe_mode")) { + urlParams.delete("safe_mode"); + } else { + urlParams.set("safe_mode", "no_themes,no_plugins"); + } + window.location.search = urlParams.toString(); + } + + +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/state.js b/app/assets/javascripts/discourse/app/static/dev-tools/state.js new file mode 100644 index 00000000000..45f35458368 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/state.js @@ -0,0 +1,10 @@ +import { tracked } from "@glimmer/tracking"; + +class DevToolsState { + @tracked pluginOutletDebug = false; +} + +const state = new DevToolsState(); +Object.preventExtensions(state); + +export default state; diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/styles.css b/app/assets/javascripts/discourse/app/static/dev-tools/styles.css new file mode 100644 index 00000000000..314a5fef293 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/styles.css @@ -0,0 +1,125 @@ +/** + This CSS file is loaded dynamically when loadDevTools() is run in the console. + It is not part of our normal CSS build process, so SCSS variables are not available. + Native CSS nesting can be used safely, because developers who use this tool are expected to have modern browsers. +*/ +.plugin-outlet-info { + --plugin-outlet-info-border-color: #080; + --plugin-outlet-info-background-color: #0c0; + + margin: 1px; + border: 1px solid var(--plugin-outlet-info-border-color); + display: inline-block; + + background-color: var(--plugin-outlet-info-background-color); + color: white; + + text-align: center; + font-size: 14px !important; + font-weight: normal; + padding: 1px 5px; + display: inline-flex; + align-items: center; + + .d-icon { + color: white !important; + font-size: 14px !important; + width: 14px !important; + } + + &.--wrapper { + --plugin-outlet-info-border-color: #00c; + --plugin-outlet-info-background-color: #88f; + } +} + +.plugin-outlet-info__wrapper { + display: flex; + flex-direction: column; +} + +.plugin-outlet-info__heading { + font-size: 14px; + font-weight: bold; + margin-bottom: 10px; + + display: flex; + gap: 5px; + + .title { + flex-grow: 1; + } + + .github-link { + color: var(--primary-medium); + } +} + +.plugin-outlet-info__content { + display: grid; + grid-template-columns: min-content 1fr; + font-size: 14px; + grid-gap: 10px; + min-width: 300px; + max-width: 100%; + width: 100%; + overflow: hidden; + + & > div { + min-width: 0; + } + + .fw { + font-family: "Courier New", "Courier", "Lucida Console", "Monaco", monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background: var(--primary-very-low); + } + + .value { + display: flex; + gap: 5px; + } + + .no-arguments { + grid-column: span 2; + } + + a { + color: var(--primary-medium); + } +} + +.dev-tools-toolbar { + position: fixed; + z-index: 999999; + + display: flex; + flex-direction: column; + + background-color: var(--primary-low); + border-radius: 0px 5px 5px 0px; + + button { + background: none; + border: none; + padding: 5px; + color: var(--primary-medium); + + &:hover:not(.gripper) { + background-color: var(--primary-very-low); + } + + &.gripper { + cursor: grab; + padding-bottom: 0; + padding-top: 0; + color: var(--primary-400); + } + + &.--active { + color: var(--success); + } + } +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/toolbar.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/toolbar.gjs new file mode 100644 index 00000000000..5f33fd09058 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/toolbar.gjs @@ -0,0 +1,85 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { htmlSafe } from "@ember/template"; +import draggable from "discourse/modifiers/draggable"; +import onResize from "discourse/modifiers/on-resize"; +import icon from "discourse-common/helpers/d-icon"; +import I18n from "discourse-i18n"; +import PluginOutletDebugButton from "./plugin-outlet-debug/button"; +import SafeModeButton from "./safe-mode/button"; +import VerboseLocalizationButton from "./verbose-localization/button"; + +export default class Toolbar extends Component { + @tracked top = 250; + @tracked ownSize = 0; + + activeDragOffset; + + get style() { + const clampedTop = Math.max(this.top, 0); + return htmlSafe(`top: min(100dvh - ${this.ownSize}px, ${clampedTop}px);`); + } + + @action + disableDevTools() { + I18n.disableVerboseLocalizationSession(); + window.disableDevTools(); + } + + @action + didStartDrag(event) { + const realTop = event.target + .closest(".dev-tools-toolbar") + .getBoundingClientRect().top; + const dragStartedAtY = event.pageY || event.touches[0].pageY; + this.activeDragOffset = dragStartedAtY - realTop; + } + + @action + didEndDrag() { + this.activeDragOffset = null; + } + + @action + dragMove(event) { + const dragY = event.pageY || event.touches[0].pageY; + this.top = dragY - this.activeDragOffset; + } + + @action + onResize(entries) { + this.ownSize = entries[0].contentRect.height; + } + + +} diff --git a/app/assets/javascripts/discourse/app/static/dev-tools/verbose-localization/button.gjs b/app/assets/javascripts/discourse/app/static/dev-tools/verbose-localization/button.gjs new file mode 100644 index 00000000000..d71a9fd4a0e --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/dev-tools/verbose-localization/button.gjs @@ -0,0 +1,31 @@ +import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import concatClass from "discourse/helpers/concat-class"; +import icon from "discourse-common/helpers/d-icon"; +import I18n from "discourse-i18n"; + +export default class VerboseLocalizationButton extends Component { + @action + toggleVerboseLocalization() { + if (I18n.verbose) { + I18n.disableVerboseLocalizationSession(); + } else { + I18n.enableVerboseLocalizationSession(); + } + window.location.reload(); + } + + +} diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index f52780a0170..711ee664808 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -25,6 +25,7 @@ "decorator-transforms": "^2.3.0", "discourse-hbr": "workspace:1.0.0", "discourse-widget-hbs": "workspace:1.0.0", + "ember-curry-component": "^0.1.0", "ember-route-template": "^1.0.3", "ember-tracked-storage-polyfill": "^1.0.0", "handlebars": "^4.7.8", diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js index e777a0b5fc2..4a704502c61 100644 --- a/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js +++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js @@ -1,2 +1,2 @@ -require("discourse-common/config/environment").setEnvironment("testing"); +require("discourse-common/config/environment").setEnvironment("qunit-testing"); require("discourse/tests/test-boot-ember-cli"); diff --git a/package.json b/package.json index b29ce894641..e7a5f777ee3 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "lint:js:fix": "eslint --fix ./app/assets/javascripts $(script/list_bundled_plugins) --no-error-on-unmatched-pattern", "lint:hbs": "ember-template-lint 'app/assets/javascripts/**/*.{gjs,hbs}' 'plugins/*/assets/javascripts/**/*.{gjs,hbs}' 'plugins/*/admin/assets/javascripts/**/*.{gjs,hbs}'", "lint:hbs:fix": "ember-template-lint 'app/assets/javascripts/**/*.{gjs,hbs}' 'plugins/*/assets/javascripts/**/*.{gjs,hbs}' 'plugins/*/admin/assets/javascripts/**/*.{gjs,hbs}' --fix", - "lint:prettier": "pnpm pprettier --list-different 'app/assets/stylesheets/**/*.scss' 'app/assets/javascripts/**/*.{js,gjs,hbs}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs,hbs}')", - "lint:prettier:fix": "pnpm prettier -w 'app/assets/stylesheets/**/*.scss' 'app/assets/javascripts/**/*.{js,gjs,hbs}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs,hbs}')", + "lint:prettier": "pnpm pprettier --list-different 'app/assets/stylesheets/**/*.scss' 'app/assets/javascripts/**/*.{js,gjs,hbs,css}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs,hbs}')", + "lint:prettier:fix": "pnpm prettier -w 'app/assets/stylesheets/**/*.scss' 'app/assets/javascripts/**/*.{js,gjs,hbs,css}' $(script/list_bundled_plugins '/assets/stylesheets/**/*.scss') $(script/list_bundled_plugins '/{assets,admin/assets,test}/javascripts/**/*.{js,gjs,hbs}')", "lttf:ignore": "lint-to-the-future ignore", "lttf:output": "lint-to-the-future output -o ./lint-progress/", "lint-progress": "pnpm lttf:output && npx html-pages ./lint-progress --no-cache", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7e26e5efe8..d57d5832d58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,6 +296,9 @@ importers: discourse-widget-hbs: specifier: workspace:1.0.0 version: link:../discourse-widget-hbs + ember-curry-component: + specifier: ^0.1.0 + version: 0.1.0(@babel/core@7.26.0) ember-route-template: specifier: ^1.0.3 version: 1.0.3 @@ -4341,6 +4344,9 @@ packages: resolution: {integrity: sha512-2UBUa5SAuPg8/kRVaiOfTwlXdeVweal1zdNPibwItrhR0IvPrXpaqwJDlEZnWKEoB+h33V0JIfiWleSG6hGkkA==} engines: {node: 10.* || >= 12.*} + ember-curry-component@0.1.0: + resolution: {integrity: sha512-gHvhO1NlH8ypOGcGfiignkIV4PHSuP5yKlBz1pkf7TjVHsdBBmHOJrvakFXqbXJjM+68DMYSobNg1/Vq0GIt+w==} + ember-decorators@6.1.1: resolution: {integrity: sha512-63vZPntPn1aqMyeNRLoYjJD+8A8obd+c2iZkJflswpDRNVIsp2m7aQdSCtPt4G0U/TEq2251g+N10maHX3rnJQ==} engines: {node: '>= 8.*'} @@ -12602,6 +12608,14 @@ snapshots: - '@babel/core' - supports-color + ember-curry-component@0.1.0(@babel/core@7.26.0): + dependencies: + '@embroider/addon-shim': 1.9.0 + decorator-transforms: 2.3.0(@babel/core@7.26.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-decorators@6.1.1: dependencies: '@ember-decorators/component': 6.1.1 diff --git a/spec/system/dev_tools_spec.rb b/spec/system/dev_tools_spec.rb new file mode 100644 index 00000000000..eaf11d2ec7c --- /dev/null +++ b/spec/system/dev_tools_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +describe "Discourse dev tools", type: :system do + it "works" do + # Open site and check it loads successfully, with no dev-tools + visit("/latest") + expect(page).to have_css("#site-logo") + expect(page).not_to have_css(".dev-tools-toolbar") + + # Enable dev tools, and wait for page to reload + page.evaluate_script("enableDevTools()") + expect(page).to have_css(".dev-tools-toolbar") + + # Turn on plugin outlet debugging, and check they appear + find(".dev-tools-toolbar .toggle-plugin-outlets").click + expect(page).to have_css(".plugin-outlet-info", minimum: 10) + + # Open a tooltip + find(".plugin-outlet-info[data-outlet-name=home-logo-contents__before]").hover + expect(page).to have_css(".plugin-outlet-info__wrapper") + + # Check the outletArgs are shown + expect(page).to have_css(".plugin-outlet-info__wrapper .key", text: "title") + expect(page).to have_css( + ".plugin-outlet-info__wrapper .value", + text: "\"#{SiteSetting.title}\"", + ) + + # Turn off plugin outlet debugging, and check they disappeared + find(".dev-tools-toolbar .toggle-plugin-outlets").click + expect(page).not_to have_css(".plugin-outlet-info") + + # Disable dev tools + find(".dev-tools-toolbar .disable-dev-tools").click + + # Check reloaded successfully + expect(page).not_to have_css(".dev-tools-toolbar") + expect(page).to have_css("#site-logo") + expect(page).not_to have_css(".dev-tools-toolbar") + end +end