Repository: bestony/logoly Branch: master Commit: 9603ac669f55 Files: 61 Total size: 116.3 KB Directory structure: gitextract_5zqv3n35/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── bug_report.md │ ├── dependabot.yml │ ├── feature_request.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── Changelog.md ├── Contributers.md ├── Contributing.md ├── LICENSE ├── README.md ├── _redirects ├── biome.json ├── index.html ├── jsconfig.json ├── package.json ├── postcss.config.js ├── public/ │ ├── ads.txt │ └── site.webmanifest ├── src/ │ ├── App.vue │ ├── __tests__/ │ │ ├── AboutView.test.js │ │ ├── ExportBtn.test.js │ │ ├── FontSelectorComponent.test.js │ │ ├── fontLoader.test.js │ │ ├── fontsConfig.test.js │ │ ├── generators.test.js │ │ ├── persistentState.test.js │ │ ├── router.test.js │ │ ├── store.test.js │ │ └── useGeneratorControls.test.js │ ├── assets/ │ │ └── iconfont/ │ │ ├── iconfont.css │ │ └── iconfont.js │ ├── components/ │ │ ├── Author.vue │ │ ├── Copyright.vue │ │ ├── Description.vue │ │ ├── ExportBtn.vue │ │ ├── Faq.vue │ │ ├── FontSelector.vue │ │ ├── Logo.vue │ │ ├── Ribbon.vue │ │ ├── Slogan.vue │ │ └── generator/ │ │ ├── Onlyfans.vue │ │ ├── Pornhub.vue │ │ └── VerticalPornHub.vue │ ├── composables/ │ │ └── useGeneratorControls.js │ ├── config/ │ │ └── fonts.js │ ├── main.js │ ├── router/ │ │ └── index.js │ ├── stores/ │ │ └── store.js │ ├── style.css │ ├── styles/ │ │ └── vuetify-settings.scss │ ├── utils/ │ │ ├── fontLoader.js │ │ └── persistentState.js │ └── views/ │ └── AboutView.vue ├── tailwind.config.js ├── vite.config.js └── vitest.setup.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [bestony] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "21:00" open-pull-requests-limit: 10 assignees: - bestony ignore: - dependency-name: y18n versions: - 4.0.1 - dependency-name: eslint-plugin-vue versions: - 7.5.0 - 7.6.0 - 7.8.0 - dependency-name: stylus-loader versions: - 4.3.3 ================================================ FILE: .github/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: branches: - master jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Test with coverage run: npm run test env: CI: true - name: Build run: npm run build ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules .DS_Store dist dist-ssr coverage *.local /cypress/videos/ /cypress/screenshots/ # Editor directories and files .vscode/* !.vscode/extensions.json .idea *.suo *.ntvs* *.njsproj *.sln *.sw? *.tsbuildinfo .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* ================================================ FILE: .prettierrc.json ================================================ { "$schema": "https://json.schemastore.org/prettierrc", "semi": true, "tabWidth": 2, "singleQuote": true, "printWidth": 100, "trailingComma": "none" } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at xiqingongzi+logoly@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: Changelog.md ================================================ # Changelog ## 2025.11.14 - Added a Vitest-based test harness (jsdom environment, global setup, and 99% coverage gates) and expanded suites for stores, generators, and font utilities. - Refactored generators to share logic via new composables, centralized font metadata/configuration, and improved selection persistence plus font-loading behavior. - Cleaned up Vuetify imports, refreshed dependencies, and tuned the Vite build for compressed outputs. - Adopted Biome for consistent formatting/linting, standardized project styles, and committed a Bun lockfile for deterministic installs. - Introduced GitHub Actions CI driven by Bun to run install, lint, test, and build steps on every push/PR. ## 2024.03.23 - Upgraded to Vue 3 - Added OnlyFans Generator - Added SVG Export ## 2020.10.08 - Added Font-Selector-Component ## 2019.05.04 - Fix Typo at Slogan.vue - Add vuex for keep text while switch layout - use Canvas export ## 2019.03.26 - add Vertical Pornhub Logo ## 2019.03.25 - Download Image with Dark Background. - Inverse prefix and suffix ## 2019.03.23 - Init Project - Download as PNG - Custom Font Style ================================================ FILE: Contributers.md ================================================ # Contributors - [Yovel Ovadia](https://github.com/yovelovadia) - [Daniel Yuldashev](https://github.com/yldshv) ================================================ FILE: Contributing.md ================================================ # Contribute to This Project ## Feature Request If you want to request for a new feature, you may open an issue and tell me what you want. If possible, attach a screenshot or an image to help me understand. [**Open an Issue**](https://github.com/bestony/logoly/issues/new?assignees=&labels=&template=feature_request.md&title=) ## Bug Report If you want to report a bug, you may open an issue. If possible, please include steps to reproduce the bug. [**Open an Issue**](https://github.com/bestony/logoly/issues/new?assignees=&labels=&template=bug_report.md&title=) ## Make Contribution If you want to contribute to this project, fork the project first, and then clone the project and make changes on your own repository, Once you pushed into your own repository, you may open a pull request. ## Local Development Setup 1. Ensure you have Node.js 18+ and npm 10+ installed. 2. Install dependencies with `npm install` (or `npm ci`). 3. Use the npm scripts (`npm run dev`, `npm run test`, `npm run lint`, `npm run build`) to work locally. > The project standardizes on npm; please avoid using Bun, pnpm, or yarn so that the single `package-lock.json` stays the source of truth. ================================================ FILE: LICENSE ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2012 Romain Lespinasse Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: README.md ================================================ > **new version is in active development at new-vue3 branch** # Logoly —— A Pornhub Flavour Logo Generator ![](https://img.shields.io/badge/Deployed%20on-Vercel-9cf) ![GitHub last commit](https://img.shields.io/github/last-commit/bestony/logoly.svg) ![GitHub issues](https://img.shields.io/github/issues/bestony/logoly.svg) ![GitHub stars](https://img.shields.io/github/stars/bestony/logoly.svg?style=social) **A Simple Online Logo Generator for People Who Want to Design Logos Easily.** ## Screenshot ![](https://i.loli.net/2019/03/24/5c96e02e97aff.png) ## Features - generate logo like **PornHub** or **OnlyFans** - download your own logo in PNG/SVG format - customize logo color - customize logo font size ## How to Use 1. open the Logoly website: [https://logoly.pro/](https://logoly.pro/) 2. edit the text in the box 3. change color & font size as you like 4. click the **Export** button to download the image ## TODO - share it on Facebook - customize fonts ## Changelog See [Changelog](Changelog.md) ## How to Contribute For those who want to request new features or submit bug reports, click [this link](https://github.com/bestony/logoly/issues/new/choose) to open a new issue. For those who want to play around with this project, read the `Get Started` section. At the end of this section, I suggest you read the [Contributing Guide](Contributing.md). ## Requirements - Node.js 18+ - npm 10+ (official package manager; please don't submit other lockfiles) ## Get Started 1. clone this project 2. install dependencies with `npm install` (or `npm ci` for a clean install) at the project root directory 3. start the development server with `npm run dev` 4. make changes 5. build with `npm run build` All scripts and the CI pipeline run with npm. Using Bun, pnpm, or yarn may create mismatched lockfiles and is not supported. ## Related Project - [Logoly.pro MiniProgram](https://github.com/GHLandy/logoly-pro) ## Sponsors [](http://www.leancloud.app/) ## LICENSE [WTFPL 2](LICENSE) ================================================ FILE: _redirects ================================================ # Netlify settings for single-page application /* /index.html 200 ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true, "ignore": ["src/assets/**"] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 }, "css": {}, "javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double", "semicolons": "always", "trailingCommas": "none" } }, "json": { "formatter": { "trailingCommas": "none" } }, "linter": { "enabled": true, "rules": { "recommended": true, "complexity": { "noExcessiveCognitiveComplexity": { "options": { "maxAllowedComplexity": 15 } } } } } } ================================================ FILE: index.html ================================================ Logoly.Pro —— A creative Logo Generator
================================================ FILE: jsconfig.json ================================================ { "compilerOptions": { "paths": { "@/*": ["./src/*"] } }, "exclude": ["node_modules", "dist"] } ================================================ FILE: package.json ================================================ { "name": "logoly", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "format": "biome format --write .", "lint": "biome lint .", "lint:fix": "biome lint --write .", "check": "biome check .", "check:fix": "biome check --write .", "test": "vitest run --coverage" }, "dependencies": { "@mdi/font": "^7.4.47", "@vueuse/core": "^10.11.0", "dom-to-image": "^2.6.0", "pinia": "^2.1.7", "vue": "^3.5.5", "vue-gtag": "^2.0.1", "vue-router": "^4.5.0", "vuetify": "^3.10.11" }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@tailwindcss/postcss": "^4.1.17", "@vitejs/plugin-vue": "^5.0.4", "@vitest/coverage-istanbul": "^4.0.8", "@vitest/coverage-v8": "^4.0.8", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.19", "jsdom": "^27.2.0", "postcss": "^8.4.38", "prettier": "^3.3.3", "sass": "^1.94.1", "stylus": "^0.64.0", "tailwindcss": "^4.1.17", "vite": "^5.4.5", "vite-plugin-compression": "^0.5.1", "vite-plugin-vuetify": "^2.1.2", "vitest": "^4.0.8" } } ================================================ FILE: postcss.config.js ================================================ export default { plugins: { '@tailwindcss/postcss': {}, autoprefixer: {} } }; ================================================ FILE: public/ads.txt ================================================ google.com, pub-9877802927933140, DIRECT, f08c47fec0942fa0 ================================================ FILE: public/site.webmanifest ================================================ { "name": "Logoly.Pro", "short_name": "Logoly", "description": "Generate Pornhub-style parody logos right in the browser and download them as PNG or SVG in seconds.", "start_url": "/", "display": "standalone", "background_color": "#050505", "theme_color": "#050505", "icons": [ { "src": "/favicon.ico", "sizes": "48x48 64x64 128x128 256x256", "type": "image/x-icon" }, { "src": "/social-share.png", "sizes": "512x512", "type": "image/png" } ] } ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/__tests__/AboutView.test.js ================================================ import { describe, it, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import AboutView from '@/views/AboutView.vue'; describe('AboutView', () => { it('links to the GitHub repository', () => { const wrapper = mount(AboutView); expect(wrapper.text()).toContain('Logoly.pro'); const link = wrapper.get('a'); expect(link.attributes('href')).toBe('https://github.com/bestony/logoly'); expect(link.attributes('target')).toBe('_blank'); }); }); ================================================ FILE: src/__tests__/ExportBtn.test.js ================================================ import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { createPinia, setActivePinia } from 'pinia'; import ExportBtn from '@/components/ExportBtn.vue'; import { useStore } from '@/stores/store'; const domToImageMock = vi.hoisted(() => ({ toPng: vi.fn(() => Promise.resolve('data:image/png;base64,mock')), toSvg: vi.fn(() => Promise.resolve('data:image/svg+xml,mock')) })); const onClickOutsideMock = vi.hoisted(() => vi.fn((_, handler) => handler())); vi.mock('dom-to-image', () => ({ __esModule: true, default: domToImageMock })); vi.mock('@vueuse/core', () => ({ onClickOutside: onClickOutsideMock })); describe('ExportBtn', () => { let OriginalImage; let clickSpy; beforeAll(() => { OriginalImage = window.Image; window.Image = class { constructor() { this.width = 100; this.height = 50; } setAttribute() {} set onload(handler) { this._onload = handler; } get onload() { return this._onload; } set src(value) { this._src = value; this._onload?.(); } }; clickSpy = vi.spyOn(window.HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); }); afterAll(() => { window.Image = OriginalImage; clickSpy.mockRestore(); }); beforeEach(() => { document.body.innerHTML = ''; setActivePinia(createPinia()); domToImageMock.toPng.mockClear(); domToImageMock.toSvg.mockClear(); onClickOutsideMock.mockClear(); }); const mountButton = () => mount(ExportBtn); const flush = () => new Promise((resolve) => setTimeout(resolve)); it('exports the editable area as PNG', async () => { const store = useStore(); store.prefix = 'Porn'; store.suffix = 'hub'; const wrapper = mountButton(); await wrapper.find('[value="png"]').trigger('click'); expect(domToImageMock.toPng).toHaveBeenCalledWith(document.getElementById('logo')); await flush(); expect(store.editable).toBe(true); expect(onClickOutsideMock).toHaveBeenCalled(); }); it('exports the editable area as SVG', async () => { const store = useStore(); store.prefix = 'Only'; store.suffix = 'Fans'; const wrapper = mountButton(); await wrapper.find('[value="svg"]').trigger('click'); expect(domToImageMock.toSvg).toHaveBeenCalledWith(document.getElementById('logo')); await flush(); expect(clickSpy).toHaveBeenCalled(); expect(store.editable).toBe(true); }); it('skips exporting when the logo node is missing', async () => { document.body.innerHTML = ''; setActivePinia(createPinia()); const wrapper = mountButton(); await wrapper.find('[value="png"]').trigger('click'); expect(domToImageMock.toPng).not.toHaveBeenCalled(); }); it('falls back to the default filename when none is provided', () => { const dispatchSpy = vi.spyOn(window.HTMLAnchorElement.prototype, 'dispatchEvent'); const wrapper = mountButton(); const { downloadImage } = wrapper.vm.$.setupState; expect(typeof downloadImage).toBe('function'); downloadImage('data:image/png;base64,stub'); expect(dispatchSpy).toHaveBeenCalled(); dispatchSpy.mockRestore(); }); }); ================================================ FILE: src/__tests__/FontSelectorComponent.test.js ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { defineComponent } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; import FontSelector from '@/components/FontSelector.vue'; import { useStore } from '@/stores/store'; import { loadGoogleFont } from '@/utils/fontLoader'; vi.mock('@/utils/fontLoader', () => ({ loadGoogleFont: vi.fn() })); const VSelectStub = defineComponent({ name: 'VSelectStub', props: { modelValue: { type: String, default: '' }, items: { type: Array, default: () => [] } }, emits: ['update:modelValue'], template: `` }); describe('FontSelector component', () => { beforeEach(() => { setActivePinia(createPinia()); vi.clearAllMocks(); }); it('loads the current font immediately and on selection changes', async () => { const wrapper = mount(FontSelector, { global: { stubs: { 'v-select': VSelectStub } } }); const store = useStore(); expect(loadGoogleFont).toHaveBeenCalledTimes(1); expect(loadGoogleFont).toHaveBeenCalledWith(store.font); loadGoogleFont.mockClear(); const select = wrapper.get('select'); await select.setValue('Lora'); expect(loadGoogleFont).toHaveBeenCalledTimes(1); expect(loadGoogleFont).toHaveBeenCalledWith('Lora'); expect(wrapper.html()).toContain('Font'); }); }); ================================================ FILE: src/__tests__/fontLoader.test.js ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; const loadModule = async () => { vi.resetModules(); const module = await import('@/utils/fontLoader.js'); return module; }; describe('loadGoogleFont', () => { beforeEach(() => { document.head.innerHTML = ''; }); it('creates a preload link and converts it to stylesheet on load', async () => { const { loadGoogleFont } = await loadModule(); loadGoogleFont(' Roboto '); const link = document.head.querySelector('link'); expect(link).toBeTruthy(); expect(link.rel).toBe('preload'); expect(link.href).toContain('Roboto'); link.onload(); expect(link.rel).toBe('stylesheet'); }); it('avoids duplicate requests while a font is pending or already loaded', async () => { const { loadGoogleFont } = await loadModule(); loadGoogleFont('Lora'); loadGoogleFont('Lora'); expect(document.head.querySelectorAll('link').length).toBe(1); const [first] = document.head.querySelectorAll('link'); first.onload(); loadGoogleFont('Lora'); expect(document.head.querySelectorAll('link').length).toBe(1); }); it('retries after an error by cleaning the pending state', async () => { const { loadGoogleFont } = await loadModule(); loadGoogleFont('Inter'); let link = document.head.querySelector('link'); expect(link).toBeTruthy(); link.onerror(); expect(document.head.querySelector('link')).toBeNull(); loadGoogleFont('Inter'); link = document.head.querySelector('link'); expect(link).toBeTruthy(); }); it('no-ops when the DOM is unavailable', async () => { const originalWindow = globalThis.window; const originalDocument = globalThis.document; vi.resetModules(); try { // @ts-expect-error override for test globalThis.window = undefined; // @ts-expect-error override for test globalThis.document = undefined; const { loadGoogleFont } = await import('@/utils/fontLoader.js'); expect(() => loadGoogleFont('Roboto')).not.toThrow(); } finally { globalThis.window = originalWindow; globalThis.document = originalDocument; vi.resetModules(); } }); }); ================================================ FILE: src/__tests__/fontsConfig.test.js ================================================ import { describe, it, expect } from 'vitest'; import { fonts } from '@/config/fonts'; describe('fonts config', () => { it('exports a de-duplicated, trimmed list', () => { expect(fonts.length).toBe(new Set(fonts).size); expect(fonts.every((font) => font === font.trim())).toBe(true); expect(fonts).toContain('Roboto'); expect(fonts).not.toContain('Roboto '); }); }); ================================================ FILE: src/__tests__/generators.test.js ================================================ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import { defineComponent, nextTick } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; import Pornhub from '@/components/generator/Pornhub.vue'; import VerticalPornHub from '@/components/generator/VerticalPornHub.vue'; import Onlyfans from '@/components/generator/Onlyfans.vue'; import { useStore } from '@/stores/store'; const TooltipStub = defineComponent({ name: 'TooltipStub', inheritAttrs: false, template: `
` }); const MenuStub = defineComponent({ name: 'MenuStub', inheritAttrs: false, template: `` }); const ColorPickerStub = defineComponent({ name: 'ColorPickerStub', inheritAttrs: false, emits: ['update:modelValue'], template: `
` }); const SliderStub = defineComponent({ name: 'SliderStub', inheritAttrs: false, emits: ['update:modelValue'], template: `
` }); const CheckboxStub = defineComponent({ name: 'CheckboxStub', inheritAttrs: false, emits: ['update:modelValue'], template: `` }); const ButtonStub = defineComponent({ name: 'ButtonStub', inheritAttrs: false, emits: ['click'], template: `` }); const IconStub = defineComponent({ name: 'IconStub', template: `` }); const generatorStubs = { ExportBtn: { template: '