├── .all-contributorsrc ├── .changeset ├── README.md └── config.json ├── .cspell ├── cambridge-dictionary-words.txt ├── code.txt ├── my-words.txt └── names.txt ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── sveltekit-embed.jpg └── workflows │ └── unit-test.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .sample.env ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps └── web │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── playwright.config.ts │ ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── index.test.ts │ ├── lib │ │ ├── icons │ │ │ ├── git-hub.svelte │ │ │ ├── index.ts │ │ │ ├── twitter.svelte │ │ │ └── you-tube.svelte │ │ ├── images │ │ │ └── spencee.png │ │ └── index.ts │ ├── prism.css │ └── routes │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── +page.md │ │ └── back-to-top.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tests │ └── test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest-setup-client.ts ├── cspell.json ├── eslint.config.js ├── package.json ├── packages └── sveltekit-embed │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── TESTING_STRATEGY.md │ ├── e2e │ └── test.ts │ ├── package.json │ ├── playwright.config.ts │ ├── src │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── components │ │ │ ├── anchor-fm.svelte │ │ │ ├── anchor-fm.svelte.test.ts │ │ │ ├── bluesky.svelte │ │ │ ├── bluesky.svelte.test.ts │ │ │ ├── buzzsprout.svelte │ │ │ ├── buzzsprout.svelte.test.ts │ │ │ ├── code-pen.svelte │ │ │ ├── code-pen.svelte.test.ts │ │ │ ├── deezer.svelte │ │ │ ├── deezer.svelte.test.ts │ │ │ ├── general-observer.svelte │ │ │ ├── general-observer.svelte.test.ts │ │ │ ├── generic-embed.svelte │ │ │ ├── generic-embed.svelte.test.ts │ │ │ ├── gist.svelte │ │ │ ├── gist.svelte.test.ts │ │ │ ├── guild.svelte │ │ │ ├── guild.svelte.test.ts │ │ │ ├── relive.svelte │ │ │ ├── relive.svelte.test.ts │ │ │ ├── simple-cast.svelte │ │ │ ├── simple-cast.svelte.test.ts │ │ │ ├── slides.svelte │ │ │ ├── slides.svelte.test.ts │ │ │ ├── sound-cloud.svelte │ │ │ ├── sound-cloud.svelte.test.ts │ │ │ ├── spotify.svelte │ │ │ ├── spotify.svelte.test.ts │ │ │ ├── stackblitz.svelte │ │ │ ├── stackblitz.svelte.test.ts │ │ │ ├── tiktok.svelte │ │ │ ├── tiktok.svelte.test.ts │ │ │ ├── toot.svelte │ │ │ ├── toot.svelte.test.ts │ │ │ ├── tweet.svelte │ │ │ ├── tweet.svelte.test.ts │ │ │ ├── vimeo.svelte │ │ │ ├── vimeo.svelte.test.ts │ │ │ ├── you-tube.svelte │ │ │ ├── you-tube.svelte.test.ts │ │ │ ├── zencastr.svelte │ │ │ └── zencastr.svelte.test.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── index.ts │ └── routes │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest-setup-client.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── renovate.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "sveltekit-embed", 3 | "projectOwner": "spences10", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "spences10", 15 | "name": "Scott Spence", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/234708?v=4", 17 | "profile": "https://scottspence.com/", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "Cahllagerfeld", 24 | "name": "Cahllagerfeld", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/43843195?v=4", 26 | "profile": "https://github.com/Cahllagerfeld", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "matiasfha", 33 | "name": "Matías Hernández Arellano", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/282006?v=4", 35 | "profile": "https://matiashernandez.dev/", 36 | "contributions": [ 37 | "code" 38 | ] 39 | }, 40 | { 41 | "login": "sphinxc0re", 42 | "name": "Julian Laubstein", 43 | "avatar_url": "https://avatars.githubusercontent.com/u/3702016?v=4", 44 | "profile": "https://ruhr.social/@sphinxc0re", 45 | "contributions": [ 46 | "code" 47 | ] 48 | }, 49 | { 50 | "login": "Ennoriel", 51 | "name": "Maxime Dupont", 52 | "avatar_url": "https://avatars.githubusercontent.com/u/23211596?v=4", 53 | "profile": "https://github.com/Ennoriel", 54 | "contributions": [ 55 | "code" 56 | ] 57 | }, 58 | { 59 | "login": "perkinsjr", 60 | "name": "James Perkins", 61 | "avatar_url": "https://avatars.githubusercontent.com/u/45409975?v=4", 62 | "profile": "https://jamesperkins.dev/", 63 | "contributions": [ 64 | "code" 65 | ] 66 | }, 67 | { 68 | "login": "joaopalmeiro", 69 | "name": "João Palmeiro", 70 | "avatar_url": "https://avatars.githubusercontent.com/u/17132927?v=4", 71 | "profile": "https://joaopalmeiro.github.io/", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "Jason3S", 78 | "name": "Jason Dent", 79 | "avatar_url": "https://avatars.githubusercontent.com/u/3740137?v=4", 80 | "profile": "https://github.com/Jason3S", 81 | "contributions": [ 82 | "code" 83 | ] 84 | } 85 | ], 86 | "contributorsPerLine": 7, 87 | "linkToUsage": true 88 | } 89 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "web" 12 | ] 13 | } -------------------------------------------------------------------------------- /.cspell/cambridge-dictionary-words.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-embed/42dd1f54bea3305cd0ffb5792a2a00072027029e/.cspell/cambridge-dictionary-words.txt -------------------------------------------------------------------------------- /.cspell/code.txt: -------------------------------------------------------------------------------- 1 | allowtransparency 2 | colspan 3 | iframe 4 | markdownlint 5 | pcss 6 | tbody 7 | testid 8 | tfoot 9 | valign 10 | webdev 11 | writable 12 | -------------------------------------------------------------------------------- /.cspell/my-words.txt: -------------------------------------------------------------------------------- 1 | genericembed 2 | mycomponent 3 | oldschool 4 | -------------------------------------------------------------------------------- /.cspell/names.txt: -------------------------------------------------------------------------------- 1 | adamwathan 2 | anchorfm 3 | Arellano 4 | Buzzsprout 5 | Cahllagerfeld 6 | codepen 7 | daisyui 8 | Deezer 9 | Drasner 10 | Ennoriel 11 | Hernández 12 | João 13 | Laubstein 14 | Mandal 15 | markdownlint 16 | Matías 17 | Maxime 18 | mdsvex 19 | Palmeiro 20 | pauliescanlon 21 | pnpm 22 | purrfect 23 | rehype 24 | sdras 25 | simplecast 26 | smartypants 27 | soundcloud 28 | Souvik 29 | spencee 30 | spences 31 | stackblitz 32 | svead 33 | sveltekit 34 | tailwindcss 35 | vieria 36 | vite 37 | vitejs 38 | youtube 39 | Zencastr 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Desktop (please complete the following information):** 31 | 32 | - OS: [e.g. iOS] 33 | - Browser [e.g. chrome, safari] 34 | - Version [e.g. 22] 35 | 36 | **Smartphone (please complete the following information):** 37 | 38 | - Device: [e.g. iPhone6] 39 | - OS: [e.g. iOS8.1] 40 | - Browser [e.g. stock browser, safari] 41 | - Version [e.g. 22] 42 | 43 | **Additional context** 44 | 45 | Add any other context about the problem here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always 12 | frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or 21 | features you've considered. 22 | 23 | **Additional context** 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /.github/sveltekit-embed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-embed/42dd1f54bea3305cd0ffb5792a2a00072027029e/.github/sveltekit-embed.jpg -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: Unit' 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | unit_tests: 11 | name: Run unit tests 12 | runs-on: ubuntu-latest 13 | container: 14 | image: mcr.microsoft.com/playwright:v1.52.0-noble 15 | options: --user 1001 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4.1.0 20 | with: 21 | version: 8.12.1 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 22.x 25 | cache: 'pnpm' 26 | - name: Install dependencies 27 | run: pnpm install 28 | - name: Build 29 | run: pnpm run build 30 | env: 31 | PUBLIC_FATHOM_ID: ${{ secrets.PUBLIC_FATHOM_ID }} 32 | PUBLIC_FATHOM_URL: ${{ secrets.PUBLIC_FATHOM_URL }} 33 | - name: Test 34 | run: pnpm run test:ci 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | .vercel 13 | .env*.local 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /icons 2 | /images -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore files for PNPM, NPM and YARN 2 | pnpm-lock.yaml 3 | package-lock.json 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 70, 6 | "arrowParens": "avoid", 7 | "proseWrap": "always", 8 | "plugins": [ 9 | "prettier-plugin-svelte", 10 | "prettier-plugin-tailwindcss" 11 | ], 12 | "overrides": [ 13 | { 14 | "files": "*.svelte", 15 | "options": { 16 | "parser": "svelte" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | PUBLIC_FATHOM_ID='' 2 | PUBLIC_FATHOM_URL='' 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "streetsidesoftware.code-spell-checker", 4 | "svelte.svelte-vscode", 5 | "bradlc.vscode-tailwindcss" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": "explicit" 5 | }, 6 | "git.enableSmartCommit": true, 7 | "git.postCommitCommand": "sync", 8 | "cSpell.words": ["tiktok"] 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Svelte Code of Conduct 2 | 3 | This project is a Svelte Community project and therefore uses the same 4 | [Code of Conduct](https://github.com/sveltejs/community/blob/main/CODE_OF_CONDUCT.md?rgh-link-date=2022-10-31T08%3A58%3A22Z) 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Want to ask a question? Check out the [discussions]. 4 | 5 | ## What should I know before I get started? 6 | 7 | Check to see if you're contribution has already been discussed. Open a 8 | question in the discussion with a suggestion of what you intend to do. 9 | 10 | ## How can I contribute 11 | 12 | All contributions are welcome, from typo corrections to new features. 13 | 14 | If you're adding a new component to `src/lib/components` then add the 15 | export to `src/lib/index.ts`. 16 | 17 | 18 | 19 | [discussions]: 20 | https://github.com/spences10/sveltekit-embed/discussions/new 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Scott Spence 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SvelteKit Embed 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 | [![MadeWithSvelte.com shield](https://madewithsvelte.com/storage/repo-shields/3786-shield.svg)](https://madewithsvelte.com/p/sveltekit-embed/shield-link) 10 | 11 | [![Tests: Unit](https://github.com/spences10/sveltekit-embed/actions/workflows/unit-test.yml/badge.svg)](https://github.com/spences10/sveltekit-embed/actions/workflows/unit-test.yml) 12 | 13 | This is a collection of embed components I use on a regular basis 14 | packaged up for use. 15 | 16 | ![sveltekit embed cover](.github/sveltekit-embed.jpg) 17 | 18 | Each component with the exception of `Toot` and `Tweet` is wrapped in 19 | an intersection observer `GeneralObserver` which will load up the 20 | component when it scrolls into the viewport. 21 | 22 | ## Use it 23 | 24 | ```bash 25 | npm i -D sveltekit-embed 26 | ``` 27 | 28 | Use like a normal Svelte component: 29 | 30 | ```html 31 | 34 | 35 | 39 | ``` 40 | 41 | ## Supported platforms 42 | 43 | - AnchorFm 44 | - Buzzsprout 45 | - CodePen 46 | - Deezer 47 | - GenericEmbed 48 | - Gist 49 | - Guild 50 | - Relive 51 | - SimpleCast 52 | - Slides 53 | - SoundCloud 54 | - Spotify 55 | - StackBlitz 56 | - Toot 57 | - Tweet 58 | - Vimeo 59 | - YouTube 60 | - Zencastr 61 | 62 | ## Got questions? 63 | 64 | [Start a discussion](https://github.com/spences10/sveltekit-embed/discussions/new) 65 | 66 | ## Something not work? 67 | 68 | Create an 69 | [issue](https://github.com/spences10/sveltekit-embed/issues/new) 70 | 71 | ## Developing locally 72 | 73 | For the `web` page and testing new components there you'll need to 74 | have empty `env` variables. Rename the `.sample.env` file to `.env`. 75 | 76 | ```bash 77 | mv .sample.env apps/web/.env 78 | ``` 79 | 80 | Create the component in the 81 | `packages/sveltekit-embed/src/lib/components` directory. 82 | 83 | Export the component from the 84 | `packages/sveltekit-embed/src/lib/index.ts` file: 85 | 86 | ```ts 87 | export { default as MyComponent } from './components/my-component.svelte'; 88 | ``` 89 | 90 | Import the component locally into the `src/routes/+page.md` file, or 91 | create your own (`+page.svelte`) page for testing: 92 | 93 | ```svelte 94 | import {MyComponent} from 'sveltekit-embed' 95 | ``` 96 | 97 | After importing the component, add it to the 98 | `Available Components List` and document it: 99 | 100 | ```markdown 101 | ## Available Components List 102 | 103 | - [MyComponent](#mycomponent) 104 | ``` 105 | 106 | ````markdown 107 | ## MyComponent 108 | 109 | Props: 110 | 111 | ```ts 112 | myComponentId: string = ''; 113 | ``` 114 | 115 | Usage: 116 | 117 | ```html 118 | 119 | ``` 120 | 121 | Output: 122 | 123 | 124 | ```` 125 | 126 | Running the dev server on the `web` page will package the changes for 127 | use in the web app. 128 | 129 | Test locally, then submit a PR 🙏 130 | 131 | ## Thanks 132 | 133 | Credit to [@pauliescanlon](https://github.com/pauliescanlon) for the 134 | original version of this project in 135 | [MDX Embed](https://github.com/pauliescanlon/mdx-embed). 136 | 137 | ## Packaging for NPM 138 | 139 | Scott, this is here for you to remember how to do this 🙃 140 | 141 | Although I detailed this in 142 | [Making npm Packages with SvelteKit](https://scottspence.com/posts/making-npm-packages-with-sveltekit) 143 | I think it's best to put it here as I always come to the README and 144 | the instructions are never there! 😅 145 | 146 | **Publish the project to NPM** 147 | 148 | ```bash 149 | # authenticate with npm 150 | npm login 151 | # bump version with npm 152 | npm version 0.0.8 153 | # package with sveltekit 154 | pnpm run package 155 | # publish 156 | npm publish 157 | # push tags to github 158 | git push --tags 159 | ``` 160 | 161 | **Publish @next package** 162 | 163 | Same procedure except use the `--tag` flag: 164 | 165 | ```bash 166 | # authenticate with npm 167 | npm login 168 | # bump version with npm 169 | npm version 0.0.13 170 | # package with sveltekit 171 | pnpm run package 172 | # publish with tag 173 | npm publish --tag next 174 | # push tags to github 175 | git push --tags 176 | ``` 177 | 178 | **Move @next package to latest** 179 | 180 | ```bash 181 | # authenticate with npm 182 | npm login 183 | # move @next to latest 184 | npm dist-tag add sveltekit-embed@0.0.13 latest 185 | ``` 186 | 187 | ## Contributors ✨ 188 | 189 | Thanks goes to these wonderful people 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 216 | 217 | 218 |
Scott Spence
Scott Spence

💻
Cahllagerfeld
Cahllagerfeld

💻
Matías Hernández Arellano
Matías Hernández Arellano

💻
Julian Laubstein
Julian Laubstein

💻
Maxime Dupont
Maxime Dupont

💻
James Perkins
James Perkins

💻
João Palmeiro
João Palmeiro

💻
Jason Dent
Jason Dent

💻
212 | 213 | Add your contributions 214 | 215 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 226 | 227 | This project follows the 228 | [all-contributors](https://github.com/all-contributors/all-contributors) 229 | specification. Contributions of any kind welcome! 230 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | .vercel 13 | .env*.local 14 | -------------------------------------------------------------------------------- /apps/web/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # web 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - update youtube for playlists 8 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # SvelteKit Embed 🌱 2 | 3 | [docs](../../README.md) 4 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "pnpm run build:packages && vite dev", 7 | "build": "pnpm run build:packages && vite build", 8 | "preview": "vite preview", 9 | "test": "npm run test:integration && npm run test:unit", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "prettier --check . && eslint .", 13 | "format": "prettier --write .", 14 | "test:integration": "playwright test", 15 | "test:unit": "vitest", 16 | "build:packages": "pnpm -r --filter=\"../../packages/*\" run build" 17 | }, 18 | "devDependencies": { 19 | "@eslint/compat": "^1.2.9", 20 | "@eslint/js": "^9.27.0", 21 | "@playwright/test": "^1.52.0", 22 | "@sveltejs/adapter-auto": "^6.0.1", 23 | "@sveltejs/kit": "^2.21.1", 24 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 25 | "@tailwindcss/typography": "^0.5.16", 26 | "@tailwindcss/vite": "^4.1.7", 27 | "@testing-library/jest-dom": "^6.6.3", 28 | "@testing-library/svelte": "^5.2.8", 29 | "daisyui": "^5.0.38", 30 | "eslint": "^9.27.0", 31 | "eslint-config-prettier": "^10.1.5", 32 | "eslint-plugin-svelte": "^3.9.0", 33 | "fathom-client": "^3.7.2", 34 | "mdsvex": "^0.12.6", 35 | "prettier": "^3.5.3", 36 | "prettier-plugin-svelte": "^3.4.0", 37 | "prettier-plugin-tailwindcss": "^0.6.11", 38 | "rehype-autolink-headings": "^7.1.0", 39 | "rehype-external-links": "^3.0.0", 40 | "rehype-slug": "^6.0.0", 41 | "svead": "^0.0.4", 42 | "svelte": "5.33.4", 43 | "svelte-check": "^4.2.1", 44 | "sveltekit-embed": "workspace:*", 45 | "tailwindcss": "^4.1.7", 46 | "typescript": "^5.8.3", 47 | "typescript-eslint": "^8.33.0", 48 | "vite": "^6.3.5", 49 | "vitest": "^3.1.4" 50 | }, 51 | "type": "module" 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173, 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /apps/web/src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* Plugin configurations */ 4 | @plugin "@tailwindcss/typography"; 5 | @plugin "daisyui" { 6 | themes: 7 | light --default, 8 | dark --prefersdark; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | interface Platform { 10 | env: { 11 | CF_PAGES?: string; 12 | }; 13 | } 14 | } 15 | } 16 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /apps/web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/git-hub.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GitHub } from './git-hub.svelte'; 2 | export { default as Twitter } from './twitter.svelte'; 3 | export { default as YouTube } from './you-tube.svelte'; 4 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/twitter.svelte: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/icons/you-tube.svelte: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/images/spencee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-embed/42dd1f54bea3305cd0ffb5792a2a00072027029e/apps/web/src/lib/images/spencee.png -------------------------------------------------------------------------------- /apps/web/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /apps/web/src/prism.css: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * Copyright (c) 2018 Sarah Drasner 4 | * Sarah Drasner's[@sdras] Night Owl 5 | * Ported by Sara vieria [@SaraVieira] 6 | * Added by Souvik Mandal [@SimpleIndian] 7 | */ 8 | 9 | code[class*='language-'], 10 | pre[class*='language-'] { 11 | color: #d6deeb; 12 | font-family: 13 | 'Victor Mono', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', 14 | monospace; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | 25 | -webkit-hyphens: none; 26 | -moz-hyphens: none; 27 | -ms-hyphens: none; 28 | hyphens: none; 29 | } 30 | 31 | pre[class*='language-']::-moz-selection, 32 | pre[class*='language-'] ::-moz-selection, 33 | code[class*='language-']::-moz-selection, 34 | code[class*='language-'] ::-moz-selection { 35 | text-shadow: none; 36 | background: rgba(29, 59, 83, 0.99); 37 | } 38 | 39 | pre[class*='language-']::selection, 40 | pre[class*='language-'] ::selection, 41 | code[class*='language-']::selection, 42 | code[class*='language-'] ::selection { 43 | text-shadow: none; 44 | background: rgba(29, 59, 83, 0.99); 45 | } 46 | 47 | @media print { 48 | code[class*='language-'], 49 | pre[class*='language-'] { 50 | text-shadow: none; 51 | } 52 | } 53 | 54 | /* Code blocks */ 55 | pre[class*='language-'] { 56 | /* padding: 1em; */ 57 | /* margin: 0.5em 0; */ 58 | overflow: auto; 59 | } 60 | 61 | :not(pre) > code[class*='language-'], 62 | pre[class*='language-'] { 63 | color: #d6deeb; 64 | background: #19212e; 65 | } 66 | 67 | :not(pre) > code[class*='language-'] { 68 | /* padding: 0.1em; */ 69 | border-radius: 0.3em; 70 | white-space: normal; 71 | } 72 | 73 | .token.comment, 74 | .token.prolog, 75 | .token.cdata { 76 | color: rgb(99, 119, 119); 77 | } 78 | 79 | .token.punctuation { 80 | color: rgb(199, 146, 234); 81 | } 82 | 83 | .namespace { 84 | color: rgb(178, 204, 214); 85 | } 86 | 87 | .token.deleted { 88 | color: rgba(239, 83, 80, 0.56); 89 | font-style: italic; 90 | } 91 | 92 | .token.symbol, 93 | .token.property { 94 | color: rgb(128, 203, 196); 95 | } 96 | 97 | .token.tag, 98 | .token.operator, 99 | .token.keyword { 100 | color: rgb(127, 219, 202); 101 | } 102 | 103 | .token.boolean { 104 | color: rgb(255, 88, 116); 105 | } 106 | 107 | .token.number { 108 | color: rgb(247, 140, 108); 109 | } 110 | 111 | .token.constant, 112 | .token.function, 113 | .token.builtin, 114 | .token.char { 115 | color: rgb(130, 170, 255); 116 | } 117 | 118 | .token.selector, 119 | .token.doctype { 120 | color: rgb(199, 146, 234); 121 | font-style: italic; 122 | } 123 | 124 | .token.attr-name, 125 | .token.inserted { 126 | color: rgb(173, 219, 103); 127 | font-style: italic; 128 | } 129 | 130 | .token.string, 131 | .token.url, 132 | .token.entity, 133 | .language-css .token.string, 134 | .style .token.string { 135 | color: rgb(173, 219, 103); 136 | } 137 | 138 | .token.class-name, 139 | .token.atrule, 140 | .token.attr-value { 141 | color: rgb(255, 203, 139); 142 | } 143 | 144 | .token.regex, 145 | .token.important, 146 | .token.variable { 147 | color: rgb(214, 222, 235); 148 | } 149 | 150 | .token.important, 151 | .token.bold { 152 | font-weight: bold; 153 | } 154 | 155 | .token.italic { 156 | font-style: italic; 157 | } 158 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | export const load = async ({ platform }) => { 2 | const is_cloudflare = platform?.env?.CF_PAGES === '1'; 3 | 4 | return { 5 | is_cloudflare, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | 33 | 49 | 50 |
51 | 52 | {#if !data.is_cloudflare} 53 | 72 | {/if} 73 | 74 |
75 | {@render children?.()} 76 |
77 | 78 | 138 | -------------------------------------------------------------------------------- /apps/web/src/routes/back-to-top.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | {#if showScrollButton} 21 | 28 | {/if} 29 | -------------------------------------------------------------------------------- /apps/web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-embed/42dd1f54bea3305cd0ffb5792a2a00072027029e/apps/web/static/favicon.png -------------------------------------------------------------------------------- /apps/web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { mdsvex } from 'mdsvex'; 4 | import autolinkHeadings from 'rehype-autolink-headings'; 5 | import rehypeExternalLinks from 'rehype-external-links'; 6 | import slugPlugin from 'rehype-slug'; 7 | 8 | const config = { 9 | preprocess: [ 10 | vitePreprocess({}), 11 | mdsvex({ 12 | extensions: ['.md'], 13 | smartypants: true, 14 | rehypePlugins: [ 15 | slugPlugin, 16 | [ 17 | autolinkHeadings, 18 | { 19 | behavior: 'wrap', 20 | }, 21 | ], 22 | [ 23 | rehypeExternalLinks, 24 | { target: '_blank', rel: 'noopener noreferrer' }, 25 | ], 26 | ], 27 | }), 28 | ], 29 | kit: { adapter: adapter() }, 30 | extensions: ['.svelte', '.md'], 31 | }; 32 | 33 | export default config; 34 | -------------------------------------------------------------------------------- /apps/web/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect( 6 | page.getByRole('heading', { name: 'Welcome to SvelteKit' }), 7 | ).toBeVisible(); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import { svelteTesting } from '@testing-library/svelte/vite'; 4 | import { defineConfig } from 'vite'; 5 | 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), sveltekit()], 8 | test: { 9 | workspace: [ 10 | { 11 | extends: './vite.config.ts', 12 | plugins: [svelteTesting()], 13 | test: { 14 | name: 'client', 15 | environment: 'jsdom', 16 | clearMocks: true, 17 | include: ['src/**/*.svelte.{test,spec}.{js,ts}'], 18 | exclude: ['src/lib/server/**'], 19 | setupFiles: ['./vitest-setup-client.ts'], 20 | }, 21 | }, 22 | { 23 | extends: './vite.config.ts', 24 | test: { 25 | name: 'server', 26 | environment: 'node', 27 | include: ['src/**/*.{test,spec}.{js,ts}'], 28 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], 29 | }, 30 | }, 31 | ], 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /apps/web/vitest-setup-client.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { vi } from 'vitest'; 3 | 4 | // required for svelte5 + jsdom as jsdom does not support matchMedia 5 | Object.defineProperty(window, 'matchMedia', { 6 | writable: true, 7 | enumerable: true, 8 | value: vi.fn().mockImplementation(query => ({ 9 | matches: false, 10 | media: query, 11 | onchange: null, 12 | addEventListener: vi.fn(), 13 | removeEventListener: vi.fn(), 14 | dispatchEvent: vi.fn(), 15 | })), 16 | }); 17 | 18 | // add more mocks here if you need them 19 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en-GB", 4 | "ignorePaths": [ 5 | "node_modules/**/*", 6 | "pnpm-lock.yaml", 7 | "package/**/*" 8 | ], 9 | "dictionaryDefinitions": [ 10 | { 11 | "name": "cambridge_dictionary", 12 | "path": ".cspell/cambridge-dictionary-words.txt", 13 | "addWords": true 14 | }, 15 | { 16 | "name": "my_words", 17 | "path": ".cspell/my-words.txt", 18 | "addWords": true 19 | }, 20 | { 21 | "name": "names", 22 | "path": ".cspell/names.txt", 23 | "addWords": true 24 | }, 25 | { 26 | "name": "code", 27 | "path": ".cspell/code.txt", 28 | "addWords": true 29 | } 30 | ], 31 | "dictionaries": [ 32 | "cambridge_dictionary", 33 | "names", 34 | "my_words", 35 | "code" 36 | ], 37 | "ignoreWords": [], 38 | "import": [] 39 | } 40 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | import svelteConfig from './svelte.config.js'; 9 | 10 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 11 | 12 | export default ts.config( 13 | includeIgnoreFile(gitignorePath), 14 | js.configs.recommended, 15 | ...ts.configs.recommended, 16 | ...svelte.configs.recommended, 17 | prettier, 18 | ...svelte.configs.prettier, 19 | { 20 | languageOptions: { 21 | globals: { ...globals.browser, ...globals.node } 22 | }, 23 | rules: { 'no-undef': 'off' } 24 | }, 25 | { 26 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 27 | languageOptions: { 28 | parserOptions: { 29 | projectService: true, 30 | extraFileExtensions: ['.svelte'], 31 | parser: ts.parser, 32 | svelteConfig 33 | } 34 | } 35 | } 36 | ); 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev:web": "pnpm run build:packages && pnpm --filter=web dev", 5 | "build:web": "pnpm run build:packages && pnpm --filter=web build", 6 | "preview:web": "pnpm run build:packages && pnpm --filter=web preview", 7 | "test:unit:web": "pnpm run build:packages && pnpm --filter=web test:unit", 8 | "test:int:web": "pnpm run build:packages && pnpm --filter=web test:integration", 9 | "coverage:web": "pnpm run build:packages && pnpm --filter=web coverage", 10 | "dev:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed dev", 11 | "build:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed build", 12 | "preview:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed preview", 13 | "test:unit:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed test:unit", 14 | "test:int:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed test:integration", 15 | "coverage:sveltekit-embed": "pnpm run build:packages && pnpm --filter=sveltekit-embed coverage", 16 | "build:packages": "pnpm -r --filter=\"./packages/*\" run build", 17 | "format": "pnpm -r --filter=\"./apps/*\" --filter=\"./packages/*\" run format", 18 | "lint": "pnpm -r --filter=\"./apps/*\" --filter=\"./packages/*\" run lint", 19 | "cspell": "cspell '**/*.md' --config cspell.json --wordsOnly", 20 | "contributors:add": "all-contributors add", 21 | "contributors:generate": "all-contributors generate", 22 | "changeset": "changeset", 23 | "version": "changeset version", 24 | "release": "pnpm run build:packages && changeset publish" 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "^2.27.12", 28 | "cspell": "^8.17.3", 29 | "prettier": "^3.5.3", 30 | "prettier-plugin-svelte": "^3.4.0", 31 | "prettier-plugin-tailwindcss": "^0.6.11" 32 | }, 33 | "pnpm": { 34 | "onlyBuiltDependencies": [ 35 | "@tailwindcss/oxide", 36 | "esbuild" 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /packages/sveltekit-embed/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | .vercel 13 | .env*.local 14 | /coverage -------------------------------------------------------------------------------- /packages/sveltekit-embed/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # sveltekit-embed 2 | 3 | ## 0.0.22 4 | 5 | ### Patch Changes 6 | 7 | - 851f998: feat: Enhance Bluesky component with unique iframe IDs and 8 | height adjustment logic 9 | 10 | - Implemented unique ID generation for each iframe instance to 11 | ensure proper height updates. 12 | - Updated message handling to only adjust height for the specific 13 | iframe that sent the message. 14 | - Added comprehensive tests to verify functionality for multiple 15 | instances and edge cases, ensuring robustness in height 16 | adjustments and iframe identification. 17 | 18 | ## 0.0.21 19 | 20 | ### Patch Changes 21 | 22 | - update testing approach on components 23 | 24 | ## 0.0.20 25 | 26 | ### Patch Changes 27 | 28 | - update youtube for playlists 29 | 30 | ## 0.0.19 31 | 32 | ### Patch Changes 33 | 34 | - init changeset 35 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/README.md: -------------------------------------------------------------------------------- 1 | # SvelteKit Embed 🌱 2 | 3 | [![npm version](https://badge.fury.io/js/sveltekit-embed.svg)](https://badge.fury.io/js/sveltekit-embed) 4 | [![npm downloads](https://img.shields.io/npm/dm/sveltekit-embed.svg)](https://www.npmjs.com/package/sveltekit-embed) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Tests: Unit](https://github.com/spences10/sveltekit-embed/actions/workflows/unit-test.yml/badge.svg)](https://github.com/spences10/sveltekit-embed/actions/workflows/unit-test.yml) 7 | 8 | A collection of embed components for SvelteKit applications. Easily 9 | embed content from popular platforms like YouTube, Spotify, Vimeo, 10 | CodePen, and many more with performant, lazy-loaded components. 11 | 12 | ## ✨ Features 13 | 14 | - 🚀 **Lazy Loading**: All components (except `Toot` and `Tweet`) use 15 | intersection observer for performance 16 | - 📱 **Responsive**: Mobile-friendly designs that adapt to different 17 | screen sizes 18 | - 🎨 **Customizable**: Flexible props to customize appearance and 19 | behavior 20 | - 🔒 **TypeScript**: Full TypeScript support with proper type 21 | definitions 22 | - ⚡ **SvelteKit Optimized**: Built specifically for SvelteKit 23 | applications 24 | - 🌐 **19 Platforms Supported**: Wide range of supported embed 25 | platforms 26 | 27 | ## 📦 Installation 28 | 29 | ```bash 30 | npm install sveltekit-embed 31 | ``` 32 | 33 | ```bash 34 | pnpm add sveltekit-embed 35 | ``` 36 | 37 | ```bash 38 | yarn add sveltekit-embed 39 | ``` 40 | 41 | ## 🚀 Quick Start 42 | 43 | Import and use any component in your Svelte/SvelteKit application: 44 | 45 | ```svelte 46 | 49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | ``` 63 | 64 | ## 🌟 Supported Platforms 65 | 66 | | Platform | Component | Description | 67 | | ---------------- | ------------------ | ----------------------------------- | 68 | | **AnchorFm** | `` | Podcast episodes from Anchor.fm | 69 | | **Buzzsprout** | `` | Podcast episodes from Buzzsprout | 70 | | **CodePen** | `` | Interactive code examples | 71 | | **Deezer** | `` | Music tracks and playlists | 72 | | **GenericEmbed** | `` | Generic iframe embed for any URL | 73 | | **Gist** | `` | GitHub Gists | 74 | | **Guild** | `` | Guild.xyz embeds | 75 | | **Relive** | `` | Relive activity summaries | 76 | | **SimpleCast** | `` | SimpleCast podcast episodes | 77 | | **Slides** | `` | Slide presentations | 78 | | **SoundCloud** | `` | Audio tracks from SoundCloud | 79 | | **Spotify** | `` | Music tracks, albums, and playlists | 80 | | **StackBlitz** | `` | Live coding environments | 81 | | **Toot** | `` | Mastodon posts | 82 | | **Tweet** | `` | Twitter/X posts | 83 | | **Vimeo** | `` | Vimeo videos | 84 | | **YouTube** | `` | YouTube videos | 85 | | **Zencastr** | `` | Zencastr podcast episodes | 86 | 87 | ## 📖 Usage Examples 88 | 89 | ### YouTube 90 | 91 | ```svelte 92 | 95 | 96 | 97 | ``` 98 | 99 | ### Spotify 100 | 101 | ```svelte 102 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | ``` 115 | 116 | ### CodePen 117 | 118 | ```svelte 119 | 122 | 123 | 129 | ``` 130 | 131 | ### Anchor.fm 132 | 133 | ```svelte 134 | 137 | 138 | 142 | ``` 143 | 144 | ## ⚡ Performance Features 145 | 146 | All embed components (except `Toot` and `Tweet`) are automatically 147 | wrapped with an intersection observer that: 148 | 149 | - Only loads the embed when it's about to enter the viewport 150 | - Reduces initial page load time 151 | - Improves Core Web Vitals scores 152 | - Saves bandwidth for users 153 | 154 | ## 🔧 TypeScript Support 155 | 156 | Full TypeScript support is included with proper type definitions for 157 | all components and their props. 158 | 159 | ```typescript 160 | import type { YouTubeProps, SpotifyProps } from 'sveltekit-embed'; 161 | ``` 162 | 163 | ## 🤝 Contributing 164 | 165 | Contributions are welcome! Please read our 166 | [contributing guidelines](https://github.com/spences10/sveltekit-embed/blob/main/CONTRIBUTING.md) 167 | and 168 | [code of conduct](https://github.com/spences10/sveltekit-embed/blob/main/CODE_OF_CONDUCT.md). 169 | 170 | ## 📝 License 171 | 172 | MIT © [Scott Spence](https://scottspence.com) 173 | 174 | ## 🙏 Credits 175 | 176 | This project was inspired by 177 | [@pauliescanlon](https://github.com/pauliescanlon)'s 178 | [MDX Embed](https://github.com/pauliescanlon/mdx-embed). 179 | 180 | ## 📋 Links 181 | 182 | - [Documentation](https://github.com/spences10/sveltekit-embed#readme) 183 | - [GitHub Repository](https://github.com/spences10/sveltekit-embed) 184 | - [Issues](https://github.com/spences10/sveltekit-embed/issues) 185 | - [Discussions](https://github.com/spences10/sveltekit-embed/discussions) 186 | 187 | --- 188 | 189 | Made with ❤️ for the Svelte community 190 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/e2e/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect( 6 | page.getByRole('heading', { name: 'Welcome to SvelteKit' }), 7 | ).toBeVisible(); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-embed", 3 | "version": "0.0.22", 4 | "author": { 5 | "name": "Scott Spence", 6 | "email": "yo@scottspence.dev", 7 | "url": "https://scottspence.com" 8 | }, 9 | "description": "A collection of embed components for SvelteKit. Easily embed content from YouTube, Spotify, Vimeo, CodePen, and many more with performant, lazy-loaded components.", 10 | "keywords": [ 11 | "svelte", 12 | "sveltekit", 13 | "embed", 14 | "components", 15 | "youtube", 16 | "spotify", 17 | "vimeo", 18 | "codepen", 19 | "deezer", 20 | "soundcloud", 21 | "twitter", 22 | "mastodon", 23 | "github", 24 | "gist", 25 | "iframe", 26 | "lazy-loading", 27 | "intersection-observer", 28 | "performance", 29 | "responsive", 30 | "typescript", 31 | "ui-components" 32 | ], 33 | "license": "MIT", 34 | "homepage": "https://github.com/spences10/sveltekit-embed#readme", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/spences10/sveltekit-embed.git", 38 | "directory": "packages/sveltekit-embed" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/spences10/sveltekit-embed/issues" 42 | }, 43 | "funding": { 44 | "type": "github", 45 | "url": "https://github.com/sponsors/spences10" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | }, 50 | "scripts": { 51 | "dev": "vite dev", 52 | "build": "vite build && npm run package", 53 | "preview": "vite preview", 54 | "package": "svelte-kit sync && svelte-package && publint", 55 | "prepublishOnly": "npm run package", 56 | "test": "npm run test:integration && npm run test:unit", 57 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 58 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 59 | "lint": "prettier --check . && eslint .", 60 | "format": "prettier --write .", 61 | "coverage": "pnpm exec vitest run --coverage", 62 | "test:integration": "playwright test", 63 | "test:unit": "vitest", 64 | "test:ci": "vitest run" 65 | }, 66 | "exports": { 67 | ".": { 68 | "types": "./dist/index.d.ts", 69 | "svelte": "./dist/index.js" 70 | } 71 | }, 72 | "files": [ 73 | "dist", 74 | "!dist/**/*.test.*", 75 | "!dist/**/*.spec.*" 76 | ], 77 | "peerDependencies": { 78 | "svelte": "^4.0.0 || ^5.0.0" 79 | }, 80 | "devDependencies": { 81 | "@eslint/compat": "^1.2.9", 82 | "@eslint/js": "^9.27.0", 83 | "@playwright/test": "^1.52.0", 84 | "@sveltejs/adapter-auto": "^6.0.1", 85 | "@sveltejs/kit": "^2.21.1", 86 | "@sveltejs/package": "^2.3.11", 87 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 88 | "@vitest/browser": "^3.1.4", 89 | "@vitest/coverage-v8": "3.1.4", 90 | "eslint": "^9.27.0", 91 | "eslint-config-prettier": "^10.1.5", 92 | "eslint-plugin-svelte": "^3.9.0", 93 | "fathom-client": "^3.7.2", 94 | "globals": "^16.1.0", 95 | "jsdom": "^26.1.0", 96 | "playwright": "^1.52.0", 97 | "prettier": "^3.5.3", 98 | "prettier-plugin-svelte": "^3.4.0", 99 | "prettier-plugin-tailwindcss": "^0.6.11", 100 | "publint": "^0.3.12", 101 | "svelte": "5.33.1", 102 | "svelte-check": "^4.2.1", 103 | "typescript": "^5.8.3", 104 | "typescript-eslint": "^8.32.1", 105 | "vite": "^6.3.5", 106 | "vitest": "^3.1.4", 107 | "vitest-browser-svelte": "^0.1.0" 108 | }, 109 | "svelte": "./dist/index.js", 110 | "types": "./dist/index.d.ts", 111 | "type": "module" 112 | } 113 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | webServer: { 5 | command: 'npm run build && PORT=4173 node build/index.js', 6 | port: 4173, 7 | }, 8 | testDir: 'e2e', 9 | }); 10 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/anchor-fm.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
28 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/anchor-fm.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import AnchorFm from '$lib/components/anchor-fm.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | let episodeUrl = 7 | 'purrfect-dev/embed/episodes/1-31---Delivering-Digital-Content-with-GraphCMS-e14g55c/a-a650v9a'; 8 | 9 | describe('AnchorFm', () => { 10 | it('mounts with episode url', async () => { 11 | const { container } = render(AnchorFm, { 12 | episodeUrl, 13 | disable_observer: true, 14 | }); 15 | expect(container).toBeTruthy(); 16 | }); 17 | 18 | it('renders iframe with episode url', async () => { 19 | render(AnchorFm, { 20 | episodeUrl, 21 | disable_observer: true, 22 | }); 23 | const iframe = page.getByTestId('anchor-fm-episode'); 24 | const expected_src = `https://anchor.fm/${episodeUrl}`; 25 | await expect.element(iframe).toHaveAttribute('src', expected_src); 26 | }); 27 | 28 | it('mounts with custom height and width', async () => { 29 | const { container } = render(AnchorFm, { 30 | episodeUrl, 31 | height: '200px', 32 | width: '50%', 33 | disable_observer: true, 34 | }); 35 | const iframe = container.querySelector('iframe'); 36 | 37 | expect(iframe?.parentElement?.style.height).toBe('200px'); 38 | expect(iframe?.parentElement?.style.width).toBe('50%'); 39 | }); 40 | 41 | it('renders with a GeneralObserver', async () => { 42 | render(AnchorFm, { 43 | episodeUrl, 44 | disable_observer: false, 45 | }); 46 | const general_observer = page.getByTestId('general-observer'); 47 | await expect.element(general_observer).toBeInTheDocument(); 48 | }); 49 | 50 | // Coverage gaps - test stubs to implement 51 | it('should handle empty episodeUrl gracefully', async () => { 52 | // Test edge case: empty or invalid episode URL 53 | render(AnchorFm, { 54 | episodeUrl: '', 55 | disable_observer: true, 56 | }); 57 | const iframe = page.getByTestId('anchor-fm-episode'); 58 | await expect 59 | .element(iframe) 60 | .toHaveAttribute('src', 'https://anchor.fm/'); 61 | await expect 62 | .element(iframe) 63 | .toHaveAttribute('title', 'anchor-fm-'); 64 | }); 65 | 66 | it('should apply default height and width when not provided', async () => { 67 | // Test default prop values (height: '100px', width: '100%') 68 | const { container } = render(AnchorFm, { 69 | episodeUrl, 70 | disable_observer: true, 71 | }); 72 | const iframe = container.querySelector('iframe'); 73 | 74 | expect(iframe?.parentElement?.style.height).toBe('100px'); 75 | expect(iframe?.parentElement?.style.width).toBe('100%'); 76 | }); 77 | 78 | it('should handle special characters in episodeUrl', async () => { 79 | // Test URL encoding and special characters 80 | const specialUrl = 81 | 'test-podcast/episodes/episode-with-special-chars-!@#$%^&*()'; 82 | render(AnchorFm, { 83 | episodeUrl: specialUrl, 84 | disable_observer: true, 85 | }); 86 | const iframe = page.getByTestId('anchor-fm-episode'); 87 | const expectedSrc = `https://anchor.fm/${specialUrl}`; 88 | await expect.element(iframe).toHaveAttribute('src', expectedSrc); 89 | await expect 90 | .element(iframe) 91 | .toHaveAttribute('title', `anchor-fm-${specialUrl}`); 92 | }); 93 | 94 | it('should have proper iframe accessibility attributes', async () => { 95 | // Test title attribute, aria-labels, etc. 96 | render(AnchorFm, { 97 | episodeUrl, 98 | disable_observer: true, 99 | }); 100 | const iframe = page.getByTestId('anchor-fm-episode'); 101 | 102 | await expect 103 | .element(iframe) 104 | .toHaveAttribute('title', `anchor-fm-${episodeUrl}`); 105 | await expect.element(iframe).toHaveAttribute('frameborder', '0'); 106 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 107 | }); 108 | 109 | it('should handle very long episodeUrl values', async () => { 110 | // Test edge case: extremely long URLs 111 | const longUrl = 'a'.repeat(1000) + '/episodes/' + 'b'.repeat(500); 112 | render(AnchorFm, { 113 | episodeUrl: longUrl, 114 | disable_observer: true, 115 | }); 116 | const iframe = page.getByTestId('anchor-fm-episode'); 117 | const expectedSrc = `https://anchor.fm/${longUrl}`; 118 | await expect.element(iframe).toHaveAttribute('src', expectedSrc); 119 | }); 120 | 121 | it('should apply custom CSS styles correctly', async () => { 122 | // Test that custom height/width styles are properly applied 123 | const customHeight = '300px'; 124 | const customWidth = '75%'; 125 | const { container } = render(AnchorFm, { 126 | episodeUrl, 127 | height: customHeight, 128 | width: customWidth, 129 | disable_observer: true, 130 | }); 131 | const iframe = container.querySelector('iframe'); 132 | const wrapperDiv = iframe?.parentElement; 133 | 134 | expect(wrapperDiv?.style.height).toBe(customHeight); 135 | expect(wrapperDiv?.style.width).toBe(customWidth); 136 | expect(wrapperDiv?.style.position).toBe('relative'); 137 | 138 | // Check iframe styles 139 | expect(iframe?.style.position).toBe('absolute'); 140 | expect(iframe?.style.top).toBe('0px'); 141 | expect(iframe?.style.left).toBe('0px'); 142 | expect(iframe?.style.width).toBe('100%'); 143 | expect(iframe?.style.height).toBe('100%'); 144 | }); 145 | 146 | it('should handle numeric height and width values', async () => { 147 | // Test passing numbers instead of strings for dimensions 148 | const { container } = render(AnchorFm, { 149 | episodeUrl, 150 | height: '250px', 151 | width: '80%', 152 | disable_observer: true, 153 | }); 154 | const iframe = container.querySelector('iframe'); 155 | 156 | // Component should handle numeric values by converting them 157 | expect(iframe?.parentElement?.style.height).toBe('250px'); 158 | expect(iframe?.parentElement?.style.width).toBe('80%'); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/bluesky.svelte: -------------------------------------------------------------------------------- 1 | 52 | 53 |
54 |
55 | 73 |
74 |
75 | 76 | 91 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/buzzsprout.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
28 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/buzzsprout.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Buzzsprout from '$lib/components/buzzsprout.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | let buzzsproutId = '12345'; 7 | 8 | describe('Buzzsprout', () => { 9 | it('mounts with buzzsproutId', async () => { 10 | const { container } = render(Buzzsprout, { 11 | buzzsproutId, 12 | disable_observer: true, 13 | }); 14 | expect(container).toBeTruthy(); 15 | }); 16 | 17 | it('renders iframe with buzzsproutId', async () => { 18 | render(Buzzsprout, { 19 | buzzsproutId, 20 | disable_observer: true, 21 | }); 22 | const iframe = page.getByTestId('buzzsprout'); 23 | const expected_src = `https://www.buzzsprout.com/${buzzsproutId}?client_source=admin&iframe=true`; 24 | await expect.element(iframe).toHaveAttribute('src', expected_src); 25 | }); 26 | 27 | it('mounts with custom height and width', async () => { 28 | const { container } = render(Buzzsprout, { 29 | buzzsproutId, 30 | height: '200px', 31 | width: '50%', 32 | disable_observer: true, 33 | }); 34 | const iframe = container.querySelector('iframe'); 35 | 36 | expect(iframe?.parentElement?.style.height).toBe('200px'); 37 | expect(iframe?.parentElement?.style.width).toBe('50%'); 38 | }); 39 | 40 | it('renders with a GeneralObserver', async () => { 41 | render(Buzzsprout, { 42 | buzzsproutId, 43 | disable_observer: false, 44 | }); 45 | const general_observer = page.getByTestId('general-observer'); 46 | await expect.element(general_observer).toBeInTheDocument(); 47 | }); 48 | 49 | // Coverage gaps - test stubs to implement 50 | it('should handle empty buzzsproutId gracefully', async () => { 51 | render(Buzzsprout, { 52 | buzzsproutId: '', 53 | disable_observer: true, 54 | }); 55 | const iframe = page.getByTestId('buzzsprout'); 56 | const expected_src = 57 | 'https://www.buzzsprout.com/?client_source=admin&iframe=true'; 58 | await expect.element(iframe).toHaveAttribute('src', expected_src); 59 | }); 60 | 61 | it('should apply default height and width when not provided', async () => { 62 | render(Buzzsprout, { 63 | buzzsproutId: 'test123', 64 | disable_observer: true, 65 | }); 66 | 67 | const iframe = page.getByTestId('buzzsprout'); 68 | const parent = iframe.element().parentElement; 69 | 70 | // Test default values: height='200px', width='100%' 71 | expect(parent?.style.height).toBe('200px'); 72 | expect(parent?.style.width).toBe('100%'); 73 | }); 74 | 75 | it('should handle special characters in buzzsproutId', async () => { 76 | const specialBuzzsproutId = 'show-123_with-dashes.and.dots'; 77 | render(Buzzsprout, { 78 | buzzsproutId: specialBuzzsproutId, 79 | disable_observer: true, 80 | }); 81 | 82 | const iframe = page.getByTestId('buzzsprout'); 83 | const expected_src = `https://www.buzzsprout.com/${specialBuzzsproutId}?client_source=admin&iframe=true`; 84 | await expect.element(iframe).toHaveAttribute('src', expected_src); 85 | }); 86 | 87 | it('should have proper iframe accessibility attributes', async () => { 88 | const testBuzzsproutId = 'accessibility-test'; 89 | render(Buzzsprout, { 90 | buzzsproutId: testBuzzsproutId, 91 | disable_observer: true, 92 | }); 93 | 94 | const iframe = page.getByTestId('buzzsprout'); 95 | 96 | // Test accessibility attributes 97 | await expect 98 | .element(iframe) 99 | .toHaveAttribute('title', `buzzsprout-${testBuzzsproutId}`); 100 | await expect.element(iframe).toHaveAttribute('frameBorder', '0'); 101 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 102 | }); 103 | 104 | it('should handle very long buzzsproutId values', async () => { 105 | const longBuzzsproutId = 'a'.repeat(100); // 100 character buzzsprout ID 106 | render(Buzzsprout, { 107 | buzzsproutId: longBuzzsproutId, 108 | disable_observer: true, 109 | }); 110 | 111 | const iframe = page.getByTestId('buzzsprout'); 112 | const expected_src = `https://www.buzzsprout.com/${longBuzzsproutId}?client_source=admin&iframe=true`; 113 | await expect.element(iframe).toHaveAttribute('src', expected_src); 114 | await expect 115 | .element(iframe) 116 | .toHaveAttribute('title', `buzzsprout-${longBuzzsproutId}`); 117 | }); 118 | 119 | it('should apply custom CSS styles correctly', async () => { 120 | render(Buzzsprout, { 121 | buzzsproutId: 'style-test', 122 | height: '300px', 123 | width: '400px', 124 | disable_observer: true, 125 | }); 126 | 127 | const iframe = page.getByTestId('buzzsprout'); 128 | const parent = iframe.element().parentElement; 129 | 130 | // Test that parent container has correct dimensions 131 | expect(parent?.style.height).toBe('300px'); 132 | expect(parent?.style.width).toBe('400px'); 133 | expect(parent?.style.position).toBe('relative'); 134 | 135 | // Test iframe styles 136 | const iframeElement = iframe.element() as HTMLIFrameElement; 137 | expect(iframeElement.style.position).toBe('absolute'); 138 | expect(iframeElement.style.top).toBe('0px'); 139 | expect(iframeElement.style.left).toBe('0px'); 140 | expect(iframeElement.style.width).toBe('100%'); 141 | expect(iframeElement.style.height).toBe('100%'); 142 | }); 143 | 144 | it('should handle numeric height and width values', async () => { 145 | // Note: TypeScript interface expects string values 146 | render(Buzzsprout, { 147 | buzzsproutId: 'numeric-test', 148 | height: '250px', 149 | width: '80%', 150 | disable_observer: true, 151 | }); 152 | 153 | const iframe = page.getByTestId('buzzsprout'); 154 | const parent = iframe.element().parentElement; 155 | 156 | expect(parent?.style.height).toBe('250px'); 157 | expect(parent?.style.width).toBe('80%'); 158 | }); 159 | 160 | it('should construct iframe src URL correctly with query parameters', async () => { 161 | const testId = 'url-construction-test'; 162 | render(Buzzsprout, { 163 | buzzsproutId: testId, 164 | disable_observer: true, 165 | }); 166 | 167 | const iframe = page.getByTestId('buzzsprout'); 168 | const iframeElement = iframe.element() as HTMLIFrameElement; 169 | 170 | // Verify exact URL construction with specific query parameters 171 | const expectedUrl = `https://www.buzzsprout.com/${testId}?client_source=admin&iframe=true`; 172 | await expect.element(iframe).toHaveAttribute('src', expectedUrl); 173 | 174 | // Also verify the URL contains the required components 175 | const actualSrc = iframeElement.src; 176 | expect(actualSrc).toContain('buzzsprout.com'); 177 | expect(actualSrc).toContain(testId); 178 | expect(actualSrc).toContain('client_source=admin'); 179 | expect(actualSrc).toContain('iframe=true'); 180 | }); 181 | 182 | it('should render with proper CSS class structure', async () => { 183 | const { container } = render(Buzzsprout, { 184 | buzzsproutId: 'class-test', 185 | disable_observer: true, 186 | }); 187 | 188 | // Check for buzzsprout-sveltekit-embed class on the container div 189 | const buzzsproutDiv = container.querySelector( 190 | '.buzzsprout-sveltekit-embed', 191 | ); 192 | expect(buzzsproutDiv).toBeTruthy(); 193 | expect( 194 | buzzsproutDiv?.classList.contains('buzzsprout-sveltekit-embed'), 195 | ).toBe(true); 196 | 197 | // Check for general-observer test id 198 | const observerDiv = container.querySelector( 199 | '[data-testid="general-observer"]', 200 | ); 201 | expect(observerDiv).toBeTruthy(); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/code-pen.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 46 | 47 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/deezer.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 36 | 37 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/deezer.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Deezer from '$lib/components/deezer.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | const theme = 'auto'; 7 | const frameSrc = 'track/1366751722'; 8 | const height = '300px'; 9 | const width = '100%'; 10 | 11 | describe('Deezer', () => { 12 | it('mounts with default props', async () => { 13 | const { container } = render(Deezer); 14 | expect(container).toBeTruthy(); 15 | }); 16 | 17 | it('renders iframe with correct src', async () => { 18 | render(Deezer, { 19 | theme, 20 | frameSrc, 21 | height, 22 | width, 23 | disable_observer: true, 24 | }); 25 | const iframe = page.getByTitle('deezer-widget'); 26 | const expected_src = `https://widget.deezer.com/widget/${theme}/${frameSrc}`; 27 | await expect.element(iframe).toHaveAttribute('src', expected_src); 28 | }); 29 | 30 | it('mounts with custom height and width', async () => { 31 | const { container } = render(Deezer, { 32 | theme, 33 | frameSrc, 34 | height: '200px', 35 | width: '50%', 36 | disable_observer: true, 37 | }); 38 | const iframe = container.querySelector('iframe'); 39 | 40 | expect(iframe?.getAttribute('style')).toContain(`height: 200px`); 41 | expect(iframe?.getAttribute('style')).toContain(`width: 50%`); 42 | }); 43 | 44 | it('renders with a GeneralObserver', async () => { 45 | render(Deezer, { 46 | theme, 47 | frameSrc, 48 | disable_observer: false, 49 | }); 50 | const general_observer = page.getByTestId('general-observer'); 51 | await expect.element(general_observer).toBeInTheDocument(); 52 | }); 53 | 54 | describe('Edge Cases', () => { 55 | it('should handle empty frameSrc gracefully', async () => { 56 | render(Deezer, { 57 | theme, 58 | frameSrc: '', 59 | disable_observer: true, 60 | }); 61 | const iframe = page.getByTitle('deezer-widget'); 62 | const element = iframe.element() as HTMLIFrameElement; 63 | 64 | // Should still construct a valid URL even with empty frameSrc 65 | expect(element.src).toBe( 66 | `https://widget.deezer.com/widget/${theme}/`, 67 | ); 68 | }); 69 | 70 | it('should handle special characters in frameSrc', async () => { 71 | const specialFrameSrc = 'track/123-test_track'; 72 | const { getByTitle } = render(Deezer, { 73 | theme, 74 | frameSrc: specialFrameSrc, 75 | disable_observer: true, 76 | }); 77 | const iframe = getByTitle('deezer-widget'); 78 | const element = iframe.element() as HTMLIFrameElement; 79 | 80 | expect(element.src).toContain(specialFrameSrc); 81 | }); 82 | 83 | it('should handle very long frameSrc values', async () => { 84 | const longFrameSrc = 'track/' + 'a'.repeat(1000); 85 | const { getByTitle } = render(Deezer, { 86 | theme, 87 | frameSrc: longFrameSrc, 88 | disable_observer: true, 89 | }); 90 | const iframe = getByTitle('deezer-widget'); 91 | const element = iframe.element() as HTMLIFrameElement; 92 | 93 | expect(element.src).toContain(longFrameSrc); 94 | }); 95 | }); 96 | 97 | describe('Default Props', () => { 98 | it('should apply default theme when not provided', async () => { 99 | const { getByTitle } = render(Deezer, { 100 | frameSrc, 101 | disable_observer: true, 102 | }); 103 | const iframe = getByTitle('deezer-widget'); 104 | const element = iframe.element() as HTMLIFrameElement; 105 | 106 | expect(element.src).toContain('widget/auto/'); 107 | }); 108 | 109 | it('should apply default border-radius styling', async () => { 110 | const { getByTitle } = render(Deezer, { 111 | theme, 112 | frameSrc, 113 | disable_observer: true, 114 | }); 115 | const iframe = getByTitle('deezer-widget'); 116 | const element = iframe.element() as HTMLIFrameElement; 117 | 118 | expect(element.style.borderRadius).toBe('0.6rem'); 119 | }); 120 | }); 121 | 122 | describe('Theme Options', () => { 123 | it.skip('should handle different theme options', async () => { 124 | const themes = ['light', 'dark', 'auto']; 125 | 126 | for (let i = 0; i < themes.length; i++) { 127 | const testTheme = themes[i]; 128 | const uniqueFrameSrc = `track/${i}`; 129 | const { getByTitle } = render(Deezer, { 130 | theme: testTheme, 131 | frameSrc: uniqueFrameSrc, 132 | disable_observer: true, 133 | }); 134 | const iframe = getByTitle('deezer-widget'); 135 | const element = iframe.element() as HTMLIFrameElement; 136 | 137 | expect(element.src).toBe( 138 | `https://widget.deezer.com/widget/${testTheme}/${uniqueFrameSrc}`, 139 | ); 140 | } 141 | }); 142 | }); 143 | 144 | describe('Custom Styling', () => { 145 | it('should apply custom iframe styles correctly', async () => { 146 | const customStyles = 'border: 2px solid red; background: blue;'; 147 | const { getByTitle } = render(Deezer, { 148 | theme, 149 | frameSrc, 150 | iframe_styles: customStyles, 151 | disable_observer: true, 152 | }); 153 | const iframe = getByTitle('deezer-widget'); 154 | const element = iframe.element() as HTMLIFrameElement; 155 | 156 | expect(element.getAttribute('style')).toBe(customStyles); 157 | }); 158 | 159 | it('should handle numeric height and width values', async () => { 160 | const { container } = render(Deezer, { 161 | theme, 162 | frameSrc, 163 | height: '400px', 164 | width: '80%', 165 | disable_observer: true, 166 | }); 167 | const iframe = container.querySelector('iframe'); 168 | 169 | expect(iframe?.style.height).toBe('400px'); 170 | expect(iframe?.style.width).toBe('80%'); 171 | }); 172 | }); 173 | 174 | describe('URL Construction', () => { 175 | it('should construct widget URL correctly', async () => { 176 | const testTheme = 'dark'; 177 | const testFrameSrc = 'playlist/123456'; 178 | const { getByTitle } = render(Deezer, { 179 | theme: testTheme, 180 | frameSrc: testFrameSrc, 181 | disable_observer: true, 182 | }); 183 | const iframe = getByTitle('deezer-widget'); 184 | const element = iframe.element() as HTMLIFrameElement; 185 | 186 | const expectedUrl = `https://widget.deezer.com/widget/${testTheme}/${testFrameSrc}`; 187 | expect(element.src).toBe(expectedUrl); 188 | }); 189 | }); 190 | 191 | describe('Accessibility', () => { 192 | it('should have proper iframe accessibility and security attributes', async () => { 193 | const { getByTitle } = render(Deezer, { 194 | theme, 195 | frameSrc, 196 | disable_observer: true, 197 | }); 198 | const iframe = getByTitle('deezer-widget'); 199 | 200 | await expect 201 | .element(iframe) 202 | .toHaveAttribute('title', 'deezer-widget'); 203 | await expect 204 | .element(iframe) 205 | .toHaveAttribute('frameborder', '0'); 206 | await expect 207 | .element(iframe) 208 | .toHaveAttribute('allowtransparency', ''); 209 | await expect 210 | .element(iframe) 211 | .toHaveAttribute('allow', 'encrypted-media; clipboard-write'); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/general-observer.svelte: -------------------------------------------------------------------------------- 1 | 5 | 55 | 56 |
57 | {#if disable_observer} 58 |
59 | {@render children?.()} 60 |
61 | {:else if loaded} 62 |
63 | {@render children?.()} 64 |
65 | {/if} 66 |
67 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/general-observer.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterEach, 3 | beforeEach, 4 | describe, 5 | expect, 6 | it, 7 | vi, 8 | } from 'vitest'; 9 | import { render } from 'vitest-browser-svelte'; 10 | import { page } from '@vitest/browser/context'; 11 | 12 | import GeneralObserver from './general-observer.svelte'; 13 | 14 | // Mock IntersectionObserver 15 | const mockIntersectionObserver = vi.fn(); 16 | const mockObserve = vi.fn(); 17 | const mockDisconnect = vi.fn(); 18 | 19 | beforeEach(() => { 20 | mockIntersectionObserver.mockReturnValue({ 21 | observe: mockObserve, 22 | disconnect: mockDisconnect, 23 | }); 24 | vi.stubGlobal('IntersectionObserver', mockIntersectionObserver); 25 | }); 26 | 27 | afterEach(() => { 28 | vi.clearAllMocks(); 29 | vi.unstubAllGlobals(); 30 | }); 31 | 32 | describe('General Observer', () => { 33 | it('mounts', async () => { 34 | const { container } = render(GeneralObserver, { 35 | disable_observer: true, 36 | children: (() => 'Test content') as any, 37 | }); 38 | expect(container).toBeTruthy(); 39 | }); 40 | 41 | describe('Observer Disabled', () => { 42 | it.skip('should render children when disable_observer is true', async () => { 43 | const testContent = 'Test content for disabled observer'; 44 | render(GeneralObserver, { 45 | disable_observer: true, 46 | children: (() => testContent) as any, 47 | }); 48 | 49 | const content = page.getByText(testContent); 50 | await expect.element(content).toBeInTheDocument(); 51 | }); 52 | }); 53 | 54 | describe('Observer Enabled', () => { 55 | it('should not render children initially when observer is enabled', async () => { 56 | const testContent = 'Test content for enabled observer'; 57 | const { container } = render(GeneralObserver, { 58 | disable_observer: false, 59 | children: (() => testContent) as any, 60 | }); 61 | 62 | // Content should not be visible initially 63 | expect(container.textContent).not.toContain(testContent); 64 | }); 65 | 66 | it('should create IntersectionObserver when supported and enabled', async () => { 67 | render(GeneralObserver, { 68 | disable_observer: false, 69 | children: (() => 'Test content') as any, 70 | }); 71 | 72 | expect(mockIntersectionObserver).toHaveBeenCalledWith( 73 | expect.any(Function), 74 | { 75 | rootMargin: '0px', 76 | threshold: 0.5, 77 | }, 78 | ); 79 | }); 80 | 81 | it('should observe root element when observer is created', async () => { 82 | render(GeneralObserver, { 83 | disable_observer: false, 84 | children: (() => 'Test content') as any, 85 | }); 86 | 87 | const rootElement = page.getByTestId('general-observer'); 88 | 89 | expect(mockObserve).toHaveBeenCalledWith(rootElement.element()); 90 | }); 91 | 92 | it('should handle custom threshold values', async () => { 93 | const customThreshold = 0.8; 94 | render(GeneralObserver, { 95 | disable_observer: false, 96 | threshold: customThreshold, 97 | children: (() => 'Test content') as any, 98 | }); 99 | 100 | expect(mockIntersectionObserver).toHaveBeenCalledWith( 101 | expect.any(Function), 102 | { 103 | rootMargin: '0px', 104 | threshold: customThreshold, 105 | }, 106 | ); 107 | }); 108 | 109 | it('should handle threshold edge cases (0, 1)', async () => { 110 | const thresholds = [0, 1]; 111 | 112 | for (const threshold of thresholds) { 113 | render(GeneralObserver, { 114 | disable_observer: false, 115 | threshold, 116 | children: (() => 'Test content') as any, 117 | }); 118 | 119 | expect(mockIntersectionObserver).toHaveBeenCalledWith( 120 | expect.any(Function), 121 | { 122 | rootMargin: '0px', 123 | threshold, 124 | }, 125 | ); 126 | } 127 | }); 128 | }); 129 | 130 | describe('IntersectionObserver Behavior', () => { 131 | it.skip('should render children when intersection threshold is met', async () => { 132 | let intersectionCallback: 133 | | ((entries: any[]) => void) 134 | | undefined; 135 | 136 | mockIntersectionObserver.mockImplementation( 137 | (callback, options) => { 138 | intersectionCallback = callback; 139 | return { 140 | observe: mockObserve, 141 | disconnect: mockDisconnect, 142 | }; 143 | }, 144 | ); 145 | 146 | const testContent = 'Content to show after intersection'; 147 | const { container } = render(GeneralObserver, { 148 | disable_observer: false, 149 | threshold: 0.5, 150 | children: (() => testContent) as any, 151 | }); 152 | 153 | // Initially, content should not be visible 154 | expect(container.textContent).not.toContain(testContent); 155 | 156 | // Simulate intersection 157 | intersectionCallback?.([ 158 | { 159 | intersectionRatio: 0.6, // Above threshold 160 | }, 161 | ]); 162 | 163 | // Wait for reactivity 164 | await new Promise(resolve => setTimeout(resolve, 10)); 165 | 166 | // Content should now be visible 167 | expect(container.textContent).toContain(testContent); 168 | }); 169 | 170 | it('should disconnect observer after successful intersection', async () => { 171 | let intersectionCallback: 172 | | ((entries: any[]) => void) 173 | | undefined; 174 | 175 | mockIntersectionObserver.mockImplementation( 176 | (callback, options) => { 177 | intersectionCallback = callback; 178 | return { 179 | observe: mockObserve, 180 | disconnect: mockDisconnect, 181 | }; 182 | }, 183 | ); 184 | 185 | render(GeneralObserver, { 186 | disable_observer: false, 187 | children: (() => 'Test content') as any, 188 | }); 189 | 190 | // Simulate intersection 191 | intersectionCallback?.([ 192 | { 193 | intersectionRatio: 0.6, 194 | }, 195 | ]); 196 | 197 | expect(mockDisconnect).toHaveBeenCalled(); 198 | }); 199 | 200 | it.skip('should handle multiple intersection entries correctly', async () => { 201 | let intersectionCallback: 202 | | ((entries: any[]) => void) 203 | | undefined; 204 | 205 | mockIntersectionObserver.mockImplementation( 206 | (callback, options) => { 207 | intersectionCallback = callback; 208 | return { 209 | observe: mockObserve, 210 | disconnect: mockDisconnect, 211 | }; 212 | }, 213 | ); 214 | 215 | const testContent = 'Multiple entries test'; 216 | const { container } = render(GeneralObserver, { 217 | disable_observer: false, 218 | threshold: 0.5, 219 | children: (() => testContent) as any, 220 | }); 221 | 222 | // Simulate multiple entries where one meets threshold 223 | intersectionCallback?.([ 224 | { intersectionRatio: 0.3 }, // Below threshold 225 | { intersectionRatio: 0.7 }, // Above threshold 226 | { intersectionRatio: 0.1 }, // Below threshold 227 | ]); 228 | 229 | await new Promise(resolve => setTimeout(resolve, 10)); 230 | 231 | // Content should be visible because one entry met threshold 232 | expect(container.textContent).toContain(testContent); 233 | }); 234 | }); 235 | 236 | describe('IntersectionObserver Not Supported', () => { 237 | it('should handle IntersectionObserver not supported gracefully', async () => { 238 | vi.stubGlobal('IntersectionObserver', undefined); 239 | 240 | const { container } = render(GeneralObserver, { 241 | disable_observer: false, 242 | children: (() => 'Fallback content') as any, 243 | }); 244 | 245 | // Should not crash and not show content 246 | expect(container.textContent).not.toContain('Fallback content'); 247 | }); 248 | }); 249 | 250 | describe('Component Lifecycle', () => { 251 | it('should cleanup observer on component unmount', async () => { 252 | const { unmount } = render(GeneralObserver, { 253 | disable_observer: false, 254 | children: (() => 'Test content') as any, 255 | }); 256 | 257 | unmount(); 258 | 259 | expect(mockDisconnect).toHaveBeenCalled(); 260 | }); 261 | }); 262 | 263 | describe('Edge Cases', () => { 264 | it('should handle missing children gracefully', async () => { 265 | const { container } = render(GeneralObserver, { 266 | disable_observer: true, 267 | children: undefined as any, 268 | }); 269 | 270 | // Should not crash 271 | expect(container).toBeTruthy(); 272 | }); 273 | 274 | it('should maintain proper data-testid on root element', async () => { 275 | render(GeneralObserver, { 276 | disable_observer: true, 277 | children: (() => 'Test content') as any, 278 | }); 279 | 280 | const rootElement = page.getByTestId('general-observer'); 281 | await expect.element(rootElement).toBeInTheDocument(); 282 | }); 283 | }); 284 | 285 | describe('Configuration', () => { 286 | it('should respect rootMargin configuration', async () => { 287 | render(GeneralObserver, { 288 | disable_observer: false, 289 | children: (() => 'Test content') as any, 290 | }); 291 | 292 | expect(mockIntersectionObserver).toHaveBeenCalledWith( 293 | expect.any(Function), 294 | { 295 | rootMargin: '0px', 296 | threshold: 0.5, 297 | }, 298 | ); 299 | }); 300 | }); 301 | }); 302 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/generic-embed.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | {@render children?.()} 28 | 29 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/generic-embed.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import GenericEmbed from '$lib/components/generic-embed.svelte'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { render } from 'vitest-browser-svelte'; 4 | import { page } from '@vitest/browser/context'; 5 | 6 | describe('GenericEmbed', () => { 7 | it('mounts with default props', async () => { 8 | const { container } = render(GenericEmbed, { 9 | src: '', 10 | title: 'Test Title', 11 | height: '152px', 12 | width: '100%', 13 | disable_observer: true, 14 | children: (() => {}) as any, 15 | }); 16 | expect(container).toBeTruthy(); 17 | }); 18 | 19 | it('renders iframe with correct src and props', async () => { 20 | const src = 'https://www.youtube.com/watch?v=o-YBDTqX_ZU'; 21 | const title = 22 | 'Rick Astley - Never Gonna Give You Up (Remastered 4K 60fps,AI)'; 23 | const height = '500px'; 24 | const width = '100%'; 25 | render(GenericEmbed, { 26 | src, 27 | title, 28 | height, 29 | width, 30 | disable_observer: true, 31 | children: (() => {}) as any, 32 | }); 33 | const iframe = page.getByTitle(title); 34 | 35 | await expect.element(iframe).toHaveAttribute('src', src); 36 | await expect.element(iframe).toHaveAttribute('height', height); 37 | await expect.element(iframe).toHaveAttribute('width', width); 38 | }); 39 | 40 | it('mounts with custom height and width', async () => { 41 | const { container } = render(GenericEmbed, { 42 | src: 'https://www.youtube.com/watch?v=o-YBDTqX_ZU', 43 | title: 44 | 'Rick Astley - Never Gonna Give You Up (Remastered 4K 60fps,AI)', 45 | height: '200px', 46 | width: '50%', 47 | disable_observer: true, 48 | children: (() => {}) as any, 49 | }); 50 | const iframe = container.querySelector('iframe'); 51 | 52 | expect(iframe).toBeTruthy(); 53 | if (iframe) { 54 | expect(iframe.getAttribute('height')).toBe('200px'); 55 | expect(iframe.getAttribute('width')).toBe('50%'); 56 | } 57 | }); 58 | 59 | it('renders with a GeneralObserver', async () => { 60 | render(GenericEmbed, { 61 | src: 'https://www.youtube.com/watch?v=o-YBDTqX_ZU', 62 | title: 63 | 'Rick Astley - Never Gonna Give You Up (Remastered 4K 60fps,AI)', 64 | height: '152px', 65 | width: '100%', 66 | disable_observer: false, 67 | children: (() => {}) as any, 68 | }); 69 | const general_observer = page.getByTestId('general-observer'); 70 | await expect.element(general_observer).toBeInTheDocument(); 71 | }); 72 | 73 | describe('Edge Cases', () => { 74 | it('should handle empty src gracefully', async () => { 75 | const { container } = render(GenericEmbed, { 76 | src: '', 77 | title: 'Test Title', 78 | height: '152px', 79 | width: '100%', 80 | disable_observer: true, 81 | children: (() => {}) as any, 82 | }); 83 | const iframe = container.querySelector('iframe'); 84 | 85 | expect(iframe).toBeTruthy(); 86 | expect(iframe?.getAttribute('src')).toBe(''); 87 | }); 88 | 89 | it('should handle special characters in src URL', async () => { 90 | const specialSrc = 91 | 'https://example.com/video?id=test¶m=value%20with%20spaces'; 92 | render(GenericEmbed, { 93 | src: specialSrc, 94 | title: 'Special URL Test', 95 | height: '152px', 96 | width: '100%', 97 | disable_observer: true, 98 | children: (() => {}) as any, 99 | }); 100 | const iframe = page.getByTitle('Special URL Test'); 101 | 102 | await expect.element(iframe).toHaveAttribute('src', specialSrc); 103 | }); 104 | 105 | it('should handle special characters in title', async () => { 106 | const specialTitle = 'Test "Title" with & symbols!'; 107 | render(GenericEmbed, { 108 | src: 'https://example.com/video', 109 | title: specialTitle, 110 | height: '152px', 111 | width: '100%', 112 | disable_observer: true, 113 | children: (() => {}) as any, 114 | }); 115 | const iframe = page.getByTitle(specialTitle); 116 | 117 | await expect 118 | .element(iframe) 119 | .toHaveAttribute('title', specialTitle); 120 | }); 121 | 122 | it('should handle very long src URLs', async () => { 123 | const longSrc = 'https://example.com/' + 'a'.repeat(1000); 124 | render(GenericEmbed, { 125 | src: longSrc, 126 | title: 'Long URL Test', 127 | height: '152px', 128 | width: '100%', 129 | disable_observer: true, 130 | children: (() => {}) as any, 131 | }); 132 | const iframe = page.getByTitle('Long URL Test'); 133 | 134 | await expect.element(iframe).toHaveAttribute('src', longSrc); 135 | }); 136 | 137 | it('should handle very long titles', async () => { 138 | const longTitle = 'Long Title '.repeat(100); 139 | render(GenericEmbed, { 140 | src: 'https://example.com/video', 141 | title: longTitle, 142 | height: '152px', 143 | width: '100%', 144 | disable_observer: true, 145 | children: (() => {}) as any, 146 | }); 147 | const iframe = page.getByTitle(longTitle); 148 | 149 | await expect 150 | .element(iframe) 151 | .toHaveAttribute('title', longTitle); 152 | }); 153 | 154 | it('should handle malformed URLs gracefully', async () => { 155 | const malformedUrl = 'not-a-valid-url'; 156 | render(GenericEmbed, { 157 | src: malformedUrl, 158 | title: 'Malformed URL Test', 159 | height: '152px', 160 | width: '100%', 161 | disable_observer: true, 162 | children: (() => {}) as any, 163 | }); 164 | const iframe = page.getByTitle('Malformed URL Test'); 165 | 166 | // Should still render with the malformed URL 167 | await expect 168 | .element(iframe) 169 | .toHaveAttribute('src', malformedUrl); 170 | }); 171 | }); 172 | 173 | describe('Default Props', () => { 174 | it('should apply default prop values when not provided', async () => { 175 | const { container } = render(GenericEmbed, { 176 | src: 'https://example.com/video', 177 | title: 'Default Props Test', 178 | height: '152px', 179 | width: '100%', 180 | disable_observer: true, 181 | children: (() => {}) as any, 182 | }); 183 | const iframe = container.querySelector('iframe'); 184 | 185 | expect(iframe).toBeTruthy(); 186 | if (iframe) { 187 | // Check default values are applied 188 | expect(iframe.getAttribute('height')).toBe('152px'); 189 | expect(iframe.getAttribute('width')).toBe('100%'); 190 | } 191 | }); 192 | }); 193 | 194 | describe('Custom Dimensions', () => { 195 | it('should handle numeric height and width values', async () => { 196 | const { container } = render(GenericEmbed, { 197 | src: 'https://example.com/video', 198 | title: 'Numeric Dimensions Test', 199 | height: '300px', 200 | width: '80%', 201 | disable_observer: true, 202 | children: (() => {}) as any, 203 | }); 204 | const iframe = container.querySelector('iframe'); 205 | 206 | expect(iframe).toBeTruthy(); 207 | if (iframe) { 208 | expect(iframe.getAttribute('height')).toBe('300px'); 209 | expect(iframe.getAttribute('width')).toBe('80%'); 210 | } 211 | }); 212 | }); 213 | 214 | describe('Security', () => { 215 | it('should have proper iframe security attributes', async () => { 216 | render(GenericEmbed, { 217 | src: 'https://example.com/video', 218 | title: 'Security Test', 219 | height: '152px', 220 | width: '100%', 221 | disable_observer: true, 222 | children: (() => {}) as any, 223 | }); 224 | const iframe = page.getByTitle('Security Test'); 225 | 226 | await expect 227 | .element(iframe) 228 | .toHaveAttribute('title', 'Security Test'); 229 | await expect 230 | .element(iframe) 231 | .toHaveAttribute('src', 'https://example.com/video'); 232 | }); 233 | }); 234 | 235 | describe('Structure', () => { 236 | it('should maintain proper iframe structure', async () => { 237 | const { container } = render(GenericEmbed, { 238 | src: 'https://example.com/video', 239 | title: 'Structure Test', 240 | height: '152px', 241 | width: '100%', 242 | disable_observer: true, 243 | children: (() => {}) as any, 244 | }); 245 | 246 | const iframe = container.querySelector('iframe'); 247 | expect(iframe).toBeTruthy(); 248 | expect(iframe?.tagName).toBe('IFRAME'); 249 | }); 250 | }); 251 | 252 | describe('Additional Attributes', () => { 253 | it('should pass through additional attributes via rest props', async () => { 254 | render(GenericEmbed, { 255 | src: 'https://example.com/video', 256 | title: 'Rest Props Test', 257 | height: '152px', 258 | width: '100%', 259 | disable_observer: true, 260 | children: (() => {}) as any, 261 | frameborder: '0', 262 | allowfullscreen: true, 263 | } as any); 264 | const iframe = page.getByTitle('Rest Props Test'); 265 | 266 | await expect 267 | .element(iframe) 268 | .toHaveAttribute('frameborder', '0'); 269 | await expect 270 | .element(iframe) 271 | .toHaveAttribute('allowfullscreen', ''); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/gist.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 30 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/gist.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Gist from '$lib/components/gist.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | const gistUri = 'gauravchl'; 7 | 8 | describe('Gist', () => { 9 | it('mounts with default props', async () => { 10 | const { container } = render(Gist); 11 | expect(container).toBeTruthy(); 12 | }); 13 | 14 | it('renders iframe with correct src', async () => { 15 | render(Gist, { 16 | gistUri, 17 | disable_observer: true, 18 | }); 19 | const iframe = page.getByTitle('gist-widget'); 20 | const expected_src = `https://gist.github.com/${gistUri}.pibb`; 21 | await expect.element(iframe).toHaveAttribute('src', expected_src); 22 | }); 23 | 24 | it('mounts with custom height and width', async () => { 25 | const { container } = render(Gist, { 26 | gistUri, 27 | height: '200px', 28 | width: '50%', 29 | disable_observer: true, 30 | }); 31 | const iframe = container.querySelector('iframe'); 32 | 33 | expect(iframe?.getAttribute('style')).toContain(`height: 200px`); 34 | expect(iframe?.getAttribute('style')).toContain(`width: 50%`); 35 | }); 36 | 37 | it('renders with a GeneralObserver', async () => { 38 | render(Gist, { 39 | gistUri, 40 | disable_observer: false, 41 | }); 42 | const general_observer = page.getByTestId('general-observer'); 43 | await expect.element(general_observer).toBeInTheDocument(); 44 | }); 45 | 46 | describe('Edge Cases', () => { 47 | it('should handle empty gistUri gracefully', async () => { 48 | render(Gist, { 49 | gistUri: '', 50 | disable_observer: true, 51 | }); 52 | const iframe = page.getByTitle('gist-widget'); 53 | const element = iframe.element() as HTMLIFrameElement; 54 | 55 | // Should still construct a valid URL even with empty gistUri 56 | expect(element.src).toBe('https://gist.github.com/.pibb'); 57 | }); 58 | 59 | it('should handle special characters in gistUri', async () => { 60 | const specialGistUri = 'user-name/gist_id-123'; 61 | render(Gist, { 62 | gistUri: specialGistUri, 63 | disable_observer: true, 64 | }); 65 | const iframe = page.getByTitle('gist-widget'); 66 | const element = iframe.element() as HTMLIFrameElement; 67 | 68 | expect(element.src).toContain(specialGistUri); 69 | expect(element.src).toBe( 70 | `https://gist.github.com/${specialGistUri}.pibb`, 71 | ); 72 | }); 73 | 74 | it('should handle very long gistUri values', async () => { 75 | const longGistUri = 'user/' + 'a'.repeat(1000); 76 | render(Gist, { 77 | gistUri: longGistUri, 78 | disable_observer: true, 79 | }); 80 | const iframe = page.getByTitle('gist-widget'); 81 | const element = iframe.element() as HTMLIFrameElement; 82 | 83 | expect(element.src).toContain(longGistUri); 84 | expect(element.src).toBe( 85 | `https://gist.github.com/${longGistUri}.pibb`, 86 | ); 87 | }); 88 | 89 | it('should handle malformed gist URIs gracefully', async () => { 90 | const malformedUri = 'not/a/valid/gist/uri'; 91 | render(Gist, { 92 | gistUri: malformedUri, 93 | disable_observer: true, 94 | }); 95 | const iframe = page.getByTitle('gist-widget'); 96 | const element = iframe.element() as HTMLIFrameElement; 97 | 98 | // Should still render with the malformed URI 99 | expect(element.src).toBe( 100 | `https://gist.github.com/${malformedUri}.pibb`, 101 | ); 102 | }); 103 | }); 104 | 105 | describe('Default Props', () => { 106 | it('should apply default height and width when not provided', async () => { 107 | const { container } = render(Gist, { 108 | gistUri, 109 | disable_observer: true, 110 | }); 111 | const iframe = container.querySelector('iframe'); 112 | 113 | expect(iframe?.style.height).toBe('320px'); 114 | expect(iframe?.style.width).toBe('100%'); 115 | }); 116 | }); 117 | 118 | describe('Custom Styling', () => { 119 | it('should apply custom iframe styles correctly', async () => { 120 | const customStyles = 'border: 2px solid red; background: blue;'; 121 | render(Gist, { 122 | gistUri, 123 | iframe_styles: customStyles, 124 | disable_observer: true, 125 | }); 126 | const iframe = page.getByTitle('gist-widget'); 127 | const element = iframe.element() as HTMLIFrameElement; 128 | 129 | expect(element.getAttribute('style')).toBe(customStyles); 130 | }); 131 | 132 | it('should handle numeric height and width values', async () => { 133 | const { container } = render(Gist, { 134 | gistUri, 135 | height: '400px', 136 | width: '80%', 137 | disable_observer: true, 138 | }); 139 | const iframe = container.querySelector('iframe'); 140 | 141 | expect(iframe?.style.height).toBe('400px'); 142 | expect(iframe?.style.width).toBe('80%'); 143 | }); 144 | }); 145 | 146 | describe('URL Construction', () => { 147 | it('should construct proper GitHub gist URL', async () => { 148 | const testGistUri = 'username/gist123456'; 149 | render(Gist, { 150 | gistUri: testGistUri, 151 | disable_observer: true, 152 | }); 153 | const iframe = page.getByTitle('gist-widget'); 154 | const element = iframe.element() as HTMLIFrameElement; 155 | 156 | const expectedUrl = `https://gist.github.com/${testGistUri}.pibb`; 157 | expect(element.src).toBe(expectedUrl); 158 | }); 159 | 160 | it('should handle gist with file parameter', async () => { 161 | const gistWithFile = 'username/gist123456?file=example.js'; 162 | render(Gist, { 163 | gistUri: gistWithFile, 164 | disable_observer: true, 165 | }); 166 | const iframe = page.getByTitle('gist-widget'); 167 | const element = iframe.element() as HTMLIFrameElement; 168 | 169 | // Note: The file parameter gets included in the URL as part of gistUri 170 | expect(element.src).toBe( 171 | `https://gist.github.com/${gistWithFile}.pibb`, 172 | ); 173 | }); 174 | }); 175 | 176 | describe('Accessibility', () => { 177 | it('should have proper iframe accessibility attributes', async () => { 178 | render(Gist, { 179 | gistUri, 180 | disable_observer: true, 181 | }); 182 | const iframe = page.getByTitle('gist-widget'); 183 | 184 | await expect 185 | .element(iframe) 186 | .toHaveAttribute('title', 'gist-widget'); 187 | }); 188 | }); 189 | 190 | describe('CSS Styling', () => { 191 | it('should apply default CSS styles from component', async () => { 192 | const { container } = render(Gist, { 193 | gistUri, 194 | disable_observer: true, 195 | }); 196 | const iframe = container.querySelector('iframe'); 197 | 198 | // The component has CSS rules that should be applied 199 | expect(iframe).toBeTruthy(); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/guild.svelte: -------------------------------------------------------------------------------- 1 | 26 | 55 | 56 | 57 |
65 | 82 |
83 |
84 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/guild.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Guild from '$lib/components/guild.svelte'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { render } from 'vitest-browser-svelte'; 4 | import { page } from '@vitest/browser/context'; 5 | 6 | describe('Guild', () => { 7 | it('mounts with default props', async () => { 8 | const { container } = render(Guild, { card_id: '1234' }); 9 | expect(container).toBeTruthy(); 10 | }); 11 | 12 | it('renders iframe with correct src', async () => { 13 | const card_id = '12345'; 14 | render(Guild, { 15 | height: '420px', 16 | width: '500px', 17 | card_id, 18 | type: 'user', 19 | display_type: 'item', 20 | disable_observer: true, 21 | }); 22 | const iframe = page.getByTestId('guild-card'); 23 | const expected_src = `https://guild.host/embeds/user/${card_id}/item`; 24 | await expect.element(iframe).toHaveAttribute('src', expected_src); 25 | }); 26 | 27 | it('mounts with custom height and width', async () => { 28 | const { container } = render(Guild, { 29 | height: '350px', 30 | width: '80%', 31 | card_id: '67890', 32 | disable_observer: true, 33 | }); 34 | const iframe = container.querySelector('iframe'); 35 | 36 | expect(iframe?.parentElement?.style.height).toBe('350px'); 37 | expect(iframe?.parentElement?.style.width).toBe('80%'); 38 | }); 39 | 40 | it('renders with a GeneralObserver', async () => { 41 | render(Guild, { 42 | height: '400px', 43 | width: '600px', 44 | card_id: 'abcde', 45 | disable_observer: false, 46 | }); 47 | const general_observer = page.getByTestId('general-observer'); 48 | await expect.element(general_observer).toBeInTheDocument(); 49 | }); 50 | 51 | it('should handle empty card_id gracefully', async () => { 52 | render(Guild, { 53 | card_id: '', 54 | disable_observer: true, 55 | }); 56 | const iframe = page.getByTestId('guild-card'); 57 | const expected_src = 'https://guild.host/embeds/guild//card'; 58 | await expect.element(iframe).toHaveAttribute('src', expected_src); 59 | }); 60 | 61 | it('should apply default prop values when not provided', async () => { 62 | const { getByTestId, container } = render(Guild, { 63 | card_id: 'test123', 64 | disable_observer: true, 65 | }); 66 | 67 | const iframe = page.getByTestId('guild-card'); 68 | const parent = iframe.element().parentElement; 69 | 70 | // Test default values: height='380px', width='100%', type='guild', display_type='card' 71 | expect(parent?.style.height).toBe('380px'); 72 | expect(parent?.style.width).toBe('100%'); 73 | await expect 74 | .element(iframe) 75 | .toHaveAttribute( 76 | 'src', 77 | 'https://guild.host/embeds/guild/test123/card', 78 | ); 79 | }); 80 | 81 | it.skip('should handle different type options', async () => { 82 | const card_id = 'test456'; 83 | 84 | // Test 'user' type 85 | const { getByTestId: getUserIframe } = render(Guild, { 86 | card_id, 87 | type: 'user', 88 | disable_observer: true, 89 | }); 90 | await expect 91 | .element(getUserIframe('guild-card')) 92 | .toHaveAttribute( 93 | 'src', 94 | `https://guild.host/embeds/user/${card_id}/card`, 95 | ); 96 | 97 | // Test 'event' type 98 | const { getByTestId: getEventIframe } = render(Guild, { 99 | card_id, 100 | type: 'event', 101 | disable_observer: true, 102 | }); 103 | await expect 104 | .element(getEventIframe('guild-card')) 105 | .toHaveAttribute( 106 | 'src', 107 | `https://guild.host/embeds/event/${card_id}/card`, 108 | ); 109 | 110 | // Test 'presentation' type 111 | const { getByTestId: getPresentationIframe } = render(Guild, { 112 | card_id, 113 | type: 'presentation', 114 | disable_observer: true, 115 | }); 116 | await expect 117 | .element(getPresentationIframe('guild-card')) 118 | .toHaveAttribute( 119 | 'src', 120 | `https://guild.host/embeds/presentation/${card_id}/card`, 121 | ); 122 | }); 123 | 124 | it.skip('should handle different display_type options', async () => { 125 | const card_id = 'test789'; 126 | 127 | // Test 'item' display type 128 | const { getByTestId: getItemIframe } = render(Guild, { 129 | card_id, 130 | display_type: 'item', 131 | disable_observer: true, 132 | }); 133 | await expect 134 | .element(getItemIframe('guild-card')) 135 | .toHaveAttribute( 136 | 'src', 137 | `https://guild.host/embeds/guild/${card_id}/item`, 138 | ); 139 | 140 | // Test 'events/latest' display type 141 | const { getByTestId: getEventsLatestIframe } = render(Guild, { 142 | card_id, 143 | display_type: 'events/latest', 144 | disable_observer: true, 145 | }); 146 | await expect 147 | .element(getEventsLatestIframe('guild-card')) 148 | .toHaveAttribute( 149 | 'src', 150 | `https://guild.host/embeds/guild/${card_id}/events/latest`, 151 | ); 152 | 153 | // Test 'presentations/upcoming' display type 154 | const { getByTestId: getPresentationsUpcomingIframe } = render( 155 | Guild, 156 | { 157 | card_id, 158 | display_type: 'presentations/upcoming', 159 | disable_observer: true, 160 | }, 161 | ); 162 | await expect 163 | .element(getPresentationsUpcomingIframe('guild-card')) 164 | .toHaveAttribute( 165 | 'src', 166 | `https://guild.host/embeds/guild/${card_id}/presentations/upcoming`, 167 | ); 168 | }); 169 | 170 | it('should construct proper Guild embed URL', async () => { 171 | render(Guild, { 172 | card_id: 'svelte-society-london', 173 | type: 'guild', 174 | display_type: 'events/upcoming', 175 | disable_observer: true, 176 | }); 177 | 178 | const iframe = page.getByTestId('guild-card'); 179 | const expected_url = 180 | 'https://guild.host/embeds/guild/svelte-society-london/events/upcoming'; 181 | await expect.element(iframe).toHaveAttribute('src', expected_url); 182 | }); 183 | 184 | it('should have proper iframe accessibility attributes', async () => { 185 | const card_id = 'accessibility-test'; 186 | render(Guild, { 187 | card_id, 188 | disable_observer: true, 189 | }); 190 | 191 | const iframe = page.getByTestId('guild-card'); 192 | 193 | // Test accessibility attributes 194 | await expect 195 | .element(iframe) 196 | .toHaveAttribute('title', `guild-card-${card_id}`); 197 | await expect.element(iframe).toHaveAttribute('frameborder', '0'); 198 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 199 | }); 200 | 201 | it('should handle special characters in card_id', async () => { 202 | const specialCardId = 203 | 'user-name-with-dashes_and_underscores.dots'; 204 | render(Guild, { 205 | card_id: specialCardId, 206 | disable_observer: true, 207 | }); 208 | 209 | const iframe = page.getByTestId('guild-card'); 210 | const expected_src = `https://guild.host/embeds/guild/${specialCardId}/card`; 211 | await expect.element(iframe).toHaveAttribute('src', expected_src); 212 | }); 213 | 214 | it('should handle very long card_id values', async () => { 215 | const longCardId = 'a'.repeat(100); // 100 character card ID 216 | render(Guild, { 217 | card_id: longCardId, 218 | disable_observer: true, 219 | }); 220 | 221 | const iframe = page.getByTestId('guild-card'); 222 | const expected_src = `https://guild.host/embeds/guild/${longCardId}/card`; 223 | await expect.element(iframe).toHaveAttribute('src', expected_src); 224 | await expect 225 | .element(iframe) 226 | .toHaveAttribute('title', `guild-card-${longCardId}`); 227 | }); 228 | 229 | it('should apply custom CSS styles correctly', async () => { 230 | render(Guild, { 231 | card_id: 'style-test', 232 | height: '500px', 233 | width: '300px', 234 | disable_observer: true, 235 | }); 236 | 237 | const iframe = page.getByTestId('guild-card'); 238 | const parent = iframe.element().parentElement; 239 | 240 | // Test that parent container has correct dimensions 241 | expect(parent?.style.height).toBe('500px'); 242 | expect(parent?.style.width).toBe('300px'); 243 | expect(parent?.style.position).toBe('relative'); 244 | 245 | // Test iframe styles 246 | await expect.element(iframe).toHaveAttribute('height', '500px'); 247 | await expect.element(iframe).toHaveAttribute('width', '300px'); 248 | expect( 249 | (iframe.element() as HTMLIFrameElement).style.position, 250 | ).toBe('absolute'); 251 | expect((iframe.element() as HTMLIFrameElement).style.top).toBe( 252 | '0px', 253 | ); 254 | expect((iframe.element() as HTMLIFrameElement).style.left).toBe( 255 | '0px', 256 | ); 257 | expect((iframe.element() as HTMLIFrameElement).style.width).toBe( 258 | '100%', 259 | ); 260 | expect((iframe.element() as HTMLIFrameElement).style.height).toBe( 261 | '100%', 262 | ); 263 | expect( 264 | (iframe.element() as HTMLIFrameElement).style.borderRadius, 265 | ).toBe('0.5rem'); 266 | }); 267 | 268 | it('should handle numeric height and width values', async () => { 269 | // Note: In Svelte, numeric values are typically converted to strings 270 | render(Guild, { 271 | card_id: 'numeric-test', 272 | height: '400px', // Already as string since TypeScript interface expects string 273 | width: '250px', // Already as string since TypeScript interface expects string 274 | disable_observer: true, 275 | }); 276 | 277 | const iframe = page.getByTestId('guild-card'); 278 | const parent = iframe.element().parentElement; 279 | 280 | expect(parent?.style.height).toBe('400px'); 281 | expect(parent?.style.width).toBe('250px'); 282 | }); 283 | 284 | it('should render with proper CSS class structure', async () => { 285 | const { container } = render(Guild, { 286 | card_id: 'class-test', 287 | disable_observer: true, 288 | }); 289 | 290 | // Check for guild-card class on the container div 291 | const guildCardDiv = container.querySelector('.guild-card'); 292 | expect(guildCardDiv).toBeTruthy(); 293 | expect(guildCardDiv?.classList.contains('guild-card')).toBe(true); 294 | 295 | // Check for general-observer test id 296 | const observerDiv = container.querySelector( 297 | '[data-testid="general-observer"]', 298 | ); 299 | expect(observerDiv).toBeTruthy(); 300 | }); 301 | }); 302 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/relive.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
28 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/relive.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Relive from '$lib/components/relive.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | const reliveId = '12345678-abcd-1234-5678-123456789abc'; 7 | 8 | describe('Relive', () => { 9 | it('mounts with default props', async () => { 10 | const { container } = render(Relive); 11 | expect(container).toBeTruthy(); 12 | }); 13 | 14 | it('renders iframe with correct src', async () => { 15 | render(Relive, { 16 | reliveId, 17 | disable_observer: true, 18 | }); 19 | const iframe = page.getByTitle(`relive-${reliveId}`); 20 | const expected_src = `https://www.relive.cc/view/${reliveId}/widget`; 21 | await expect.element(iframe).toHaveAttribute('src', expected_src); 22 | }); 23 | 24 | it.skip('mounts with custom width', async () => { 25 | const customWidth = '50%'; 26 | const { container } = render(Relive, { 27 | reliveId, 28 | width: customWidth, 29 | disable_observer: true, 30 | }); 31 | const wrapper = container.querySelector('div'); 32 | 33 | expect(wrapper?.getAttribute('style')).toContain( 34 | `width: ${customWidth}`, 35 | ); 36 | }); 37 | 38 | it('renders with a GeneralObserver', async () => { 39 | render(Relive, { 40 | reliveId, 41 | disable_observer: false, 42 | }); 43 | const general_observer = page.getByTestId('general-observer'); 44 | await expect.element(general_observer).toBeInTheDocument(); 45 | }); 46 | 47 | describe('Edge Cases', () => { 48 | it('should handle empty reliveId gracefully', async () => { 49 | const { container } = render(Relive, { 50 | reliveId: '', 51 | disable_observer: true, 52 | }); 53 | const iframe = container.querySelector('iframe'); 54 | 55 | expect(iframe?.getAttribute('src')).toBe( 56 | 'https://www.relive.cc/view//widget', 57 | ); 58 | expect(iframe?.getAttribute('title')).toBe('relive-'); 59 | }); 60 | 61 | it('should handle special characters in reliveId', async () => { 62 | const specialReliveId = 'test-id_123'; 63 | render(Relive, { 64 | reliveId: specialReliveId, 65 | disable_observer: true, 66 | }); 67 | const iframe = page.getByTitle(`relive-${specialReliveId}`); 68 | const element = iframe.element() as HTMLIFrameElement; 69 | 70 | expect(element.src).toBe( 71 | `https://www.relive.cc/view/${specialReliveId}/widget`, 72 | ); 73 | }); 74 | 75 | it('should handle very long reliveId values', async () => { 76 | const longReliveId = 'a'.repeat(1000); 77 | render(Relive, { 78 | reliveId: longReliveId, 79 | disable_observer: true, 80 | }); 81 | const iframe = page.getByTitle(`relive-${longReliveId}`); 82 | const element = iframe.element() as HTMLIFrameElement; 83 | 84 | expect(element.src).toContain(longReliveId); 85 | }); 86 | 87 | it('should handle malformed reliveId gracefully', async () => { 88 | const malformedId = 'not/a/valid/id'; 89 | render(Relive, { 90 | reliveId: malformedId, 91 | disable_observer: true, 92 | }); 93 | const iframe = page.getByTitle(`relive-${malformedId}`); 94 | const element = iframe.element() as HTMLIFrameElement; 95 | 96 | // Should still render with the malformed ID 97 | expect(element.src).toBe( 98 | `https://www.relive.cc/view/${malformedId}/widget`, 99 | ); 100 | }); 101 | }); 102 | 103 | describe('Default Props', () => { 104 | it.skip('should apply default width when not provided', async () => { 105 | const { container } = render(Relive, { 106 | reliveId, 107 | disable_observer: true, 108 | }); 109 | const wrapper = container.querySelector('div'); 110 | 111 | expect(wrapper?.getAttribute('style')).toContain('width: 100%'); 112 | }); 113 | }); 114 | 115 | describe('Layout and Styling', () => { 116 | it.skip('should apply proper aspect ratio and positioning', async () => { 117 | const { container } = render(Relive, { 118 | reliveId, 119 | disable_observer: true, 120 | }); 121 | const wrapper = container.querySelector('div'); 122 | const iframe = container.querySelector('iframe'); 123 | 124 | // Check wrapper styling via style attribute 125 | const wrapperStyle = wrapper?.getAttribute('style') || ''; 126 | expect(wrapperStyle).toContain('position: relative'); 127 | expect(wrapperStyle).toContain('aspect-ratio: 1 / 0.7825'); 128 | expect(wrapperStyle).toContain('overflow: hidden'); 129 | 130 | // Check iframe positioning via style attribute 131 | const iframeStyle = iframe?.getAttribute('style') || ''; 132 | expect(iframeStyle).toContain('position: absolute'); 133 | expect(iframeStyle).toContain('top: -2px'); 134 | expect(iframeStyle).toContain('left: -2px'); 135 | expect(iframeStyle).toContain('width: calc(100% + 4px)'); 136 | expect(iframeStyle).toContain('height: calc(100% + 4px)'); 137 | }); 138 | 139 | it.skip('should handle custom width values', async () => { 140 | const widths = ['50%', '300px', '80vw']; 141 | 142 | for (const width of widths) { 143 | const { container } = render(Relive, { 144 | reliveId: `${reliveId}-${width}`, 145 | width, 146 | disable_observer: true, 147 | }); 148 | const wrapper = container.querySelector('div'); 149 | 150 | expect(wrapper?.getAttribute('style')).toContain( 151 | `width: ${width}`, 152 | ); 153 | } 154 | }); 155 | }); 156 | 157 | describe('URL Construction', () => { 158 | it('should construct proper Relive widget URL', async () => { 159 | const testReliveId = 'test-123-abc'; 160 | render(Relive, { 161 | reliveId: testReliveId, 162 | disable_observer: true, 163 | }); 164 | const iframe = page.getByTitle(`relive-${testReliveId}`); 165 | const element = iframe.element() as HTMLIFrameElement; 166 | 167 | const expectedUrl = `https://www.relive.cc/view/${testReliveId}/widget`; 168 | expect(element.src).toBe(expectedUrl); 169 | }); 170 | }); 171 | 172 | describe('Accessibility and Security', () => { 173 | it('should have proper iframe accessibility and security attributes', async () => { 174 | render(Relive, { 175 | reliveId, 176 | disable_observer: true, 177 | }); 178 | const iframe = page.getByTitle(`relive-${reliveId}`); 179 | 180 | await expect 181 | .element(iframe) 182 | .toHaveAttribute('title', `relive-${reliveId}`); 183 | await expect 184 | .element(iframe) 185 | .toHaveAttribute('frameborder', '0'); 186 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 187 | await expect 188 | .element(iframe) 189 | .toHaveAttribute('allowfullscreen', ''); 190 | }); 191 | }); 192 | 193 | describe('Component Structure', () => { 194 | it('should maintain proper wrapper and iframe structure', async () => { 195 | const { container } = render(Relive, { 196 | reliveId, 197 | disable_observer: true, 198 | }); 199 | 200 | const wrapper = container.querySelector('div'); 201 | const iframe = container.querySelector('iframe'); 202 | 203 | expect(wrapper).toBeTruthy(); 204 | expect(iframe).toBeTruthy(); 205 | expect(wrapper?.contains(iframe)).toBe(true); 206 | }); 207 | }); 208 | 209 | describe('Constants', () => { 210 | it('should use correct default margin value', async () => { 211 | const { container } = render(Relive, { 212 | reliveId, 213 | disable_observer: true, 214 | }); 215 | const iframe = container.querySelector('iframe'); 216 | 217 | // Check that the default margin is 2px via style attribute 218 | const iframeStyle = iframe?.getAttribute('style') || ''; 219 | expect(iframeStyle).toContain('top: -2px'); 220 | expect(iframeStyle).toContain('left: -2px'); 221 | expect(iframeStyle).toContain('width: calc(100% + 4px)'); 222 | expect(iframeStyle).toContain('height: calc(100% + 4px)'); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/simple-cast.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 |
26 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/simple-cast.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import SimpleCast from '$lib/components/simple-cast.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | describe('SimpleCast', () => { 7 | it('mounts with default props', async () => { 8 | const { container } = render(SimpleCast); 9 | expect(container).toBeTruthy(); 10 | }); 11 | 12 | it('renders iframe with correct src', async () => { 13 | const episodeId = '12345'; 14 | render(SimpleCast, { 15 | episodeId, 16 | theme: 'dark', 17 | disable_observer: true, 18 | }); 19 | const iframe = page.getByTestId('simplecast-episode'); 20 | const expected_src = `https://player.simplecast.com/${episodeId}?dark=true`; 21 | await expect.element(iframe).toHaveAttribute('src', expected_src); 22 | }); 23 | 24 | it('renders with a GeneralObserver', async () => { 25 | render(SimpleCast, { 26 | episodeId: '67890', 27 | disable_observer: false, 28 | }); 29 | const general_observer = page.getByTestId('general-observer'); 30 | await expect.element(general_observer).toBeInTheDocument(); 31 | }); 32 | 33 | // Coverage gaps - test stubs to implement 34 | it.skip('should handle empty episodeId gracefully', async () => { 35 | render(SimpleCast, { 36 | episodeId: '', 37 | disable_observer: true, 38 | }); 39 | const iframe = page.getByTestId('simplecast-episode'); 40 | const expected_src = 'https://player.simplecast.com/'; 41 | await expect.element(iframe).toHaveAttribute('src', expected_src); 42 | }); 43 | 44 | it('should apply default prop values when not provided', async () => { 45 | render(SimpleCast, { 46 | disable_observer: true, 47 | }); 48 | const iframe = page.getByTestId('simplecast-episode'); 49 | // Default episodeId is '', theme is 'dark' 50 | const expected_src = 'https://player.simplecast.com/?dark=true'; 51 | await expect.element(iframe).toHaveAttribute('src', expected_src); 52 | await expect 53 | .element(iframe) 54 | .toHaveAttribute('title', 'simplecast-'); 55 | }); 56 | 57 | it.skip('should handle different theme options', async () => { 58 | const episodeId = 'test123'; 59 | 60 | // Test light theme 61 | render(SimpleCast, { 62 | episodeId, 63 | theme: 'light', 64 | disable_observer: true, 65 | }); 66 | const lightIframe = page.getByTestId('simplecast-episode'); 67 | await expect 68 | .element(lightIframe) 69 | .toHaveAttribute( 70 | 'src', 71 | `https://player.simplecast.com/${episodeId}`, 72 | ); 73 | 74 | // Test dark theme 75 | render(SimpleCast, { 76 | episodeId, 77 | theme: 'dark', 78 | disable_observer: true, 79 | }); 80 | const darkIframe = page.getByTestId('simplecast-episode'); 81 | await expect 82 | .element(darkIframe) 83 | .toHaveAttribute( 84 | 'src', 85 | `https://player.simplecast.com/${episodeId}?dark=true`, 86 | ); 87 | }); 88 | 89 | it('should construct proper SimpleCast player URL', async () => { 90 | const episodeId = 'abc123def'; 91 | render(SimpleCast, { 92 | episodeId, 93 | theme: 'dark', 94 | disable_observer: true, 95 | }); 96 | const iframe = page.getByTestId('simplecast-episode'); 97 | const expected_src = `https://player.simplecast.com/${episodeId}?dark=true`; 98 | await expect.element(iframe).toHaveAttribute('src', expected_src); 99 | }); 100 | 101 | it('should handle special characters in episodeId', async () => { 102 | const episodeId = 'test-episode_123'; 103 | render(SimpleCast, { 104 | episodeId, 105 | theme: 'light', 106 | disable_observer: true, 107 | }); 108 | const iframe = page.getByTestId('simplecast-episode'); 109 | const expected_src = `https://player.simplecast.com/${episodeId}`; 110 | await expect.element(iframe).toHaveAttribute('src', expected_src); 111 | await expect 112 | .element(iframe) 113 | .toHaveAttribute('title', `simplecast-${episodeId}`); 114 | }); 115 | 116 | it('should have proper iframe accessibility attributes', async () => { 117 | const episodeId = 'accessibility-test'; 118 | render(SimpleCast, { 119 | episodeId, 120 | disable_observer: true, 121 | }); 122 | const iframe = page.getByTestId('simplecast-episode'); 123 | await expect 124 | .element(iframe) 125 | .toHaveAttribute('title', `simplecast-${episodeId}`); 126 | await expect.element(iframe).toHaveAttribute('frameBorder', 'no'); 127 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 128 | await expect.element(iframe).toHaveAttribute('seamless'); 129 | }); 130 | 131 | it('should handle very long episodeId values', async () => { 132 | const episodeId = 'a'.repeat(100); 133 | render(SimpleCast, { 134 | episodeId, 135 | theme: 'light', 136 | disable_observer: true, 137 | }); 138 | const iframe = page.getByTestId('simplecast-episode'); 139 | const expected_src = `https://player.simplecast.com/${episodeId}`; 140 | await expect.element(iframe).toHaveAttribute('src', expected_src); 141 | }); 142 | 143 | it('should apply custom CSS styles correctly', async () => { 144 | render(SimpleCast, { 145 | episodeId: 'style-test', 146 | disable_observer: true, 147 | }); 148 | const iframe = page.getByTestId('simplecast-episode'); 149 | const iframeElement = iframe.element() as HTMLIFrameElement; 150 | const styles = iframeElement.style; 151 | 152 | expect(styles.position).toBe('absolute'); 153 | expect(styles.top).toBe('0px'); 154 | expect(styles.left).toBe('0px'); 155 | expect(styles.width).toBe('100%'); 156 | expect(styles.height).toBe('100%'); 157 | }); 158 | 159 | it('should handle numeric height and width values', async () => { 160 | const { container } = render(SimpleCast, { 161 | episodeId: 'numeric-test', 162 | disable_observer: true, 163 | }); 164 | 165 | const embedDiv = container.querySelector( 166 | '.simplecast-episode-svelte-embed', 167 | ); 168 | expect(embedDiv).toBeTruthy(); 169 | const styles = (embedDiv as HTMLElement)?.style; 170 | expect(styles?.height).toBe('200px'); 171 | expect(styles?.width).toBe('100%'); 172 | }); 173 | 174 | it('should handle malformed episode IDs gracefully', async () => { 175 | const episodeId = '12345/malformed?test=true&other=false'; 176 | render(SimpleCast, { 177 | episodeId, 178 | theme: 'dark', 179 | disable_observer: true, 180 | }); 181 | const iframe = page.getByTestId('simplecast-episode'); 182 | const expected_src = `https://player.simplecast.com/${episodeId}?dark=true`; 183 | await expect.element(iframe).toHaveAttribute('src', expected_src); 184 | }); 185 | 186 | it('should render with proper CSS class structure', async () => { 187 | const { container } = render(SimpleCast, { 188 | episodeId: 'class-test', 189 | disable_observer: true, 190 | }); 191 | 192 | const embedDiv = container.querySelector( 193 | '.simplecast-episode-svelte-embed', 194 | ); 195 | expect(embedDiv).toBeTruthy(); 196 | expect(embedDiv?.className).toBe( 197 | 'simplecast-episode-svelte-embed', 198 | ); 199 | }); 200 | 201 | it.skip('should handle theme parameter in URL correctly', async () => { 202 | const episodeId = 'url-test'; 203 | 204 | // Test that non-dark themes don't add the parameter 205 | render(SimpleCast, { 206 | episodeId, 207 | theme: 'light', 208 | disable_observer: true, 209 | }); 210 | const lightIframe = page.getByTestId('simplecast-episode'); 211 | await expect 212 | .element(lightIframe) 213 | .toHaveAttribute( 214 | 'src', 215 | `https://player.simplecast.com/${episodeId}`, 216 | ); 217 | 218 | // Test that dark theme adds the parameter 219 | render(SimpleCast, { 220 | episodeId, 221 | theme: 'dark', 222 | disable_observer: true, 223 | }); 224 | const darkIframe = page.getByTestId('simplecast-episode'); 225 | await expect 226 | .element(darkIframe) 227 | .toHaveAttribute( 228 | 'src', 229 | `https://player.simplecast.com/${episodeId}?dark=true`, 230 | ); 231 | 232 | // Test custom theme value 233 | render(SimpleCast, { 234 | episodeId, 235 | theme: 'custom', 236 | disable_observer: true, 237 | }); 238 | const customIframe = page.getByTestId('simplecast-episode'); 239 | await expect 240 | .element(customIframe) 241 | .toHaveAttribute( 242 | 'src', 243 | `https://player.simplecast.com/${episodeId}`, 244 | ); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/slides.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | 48 | 49 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/sound-cloud.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 34 | 35 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/sound-cloud.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import SoundCloud from '$lib/components/sound-cloud.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | describe('SoundCloud', () => { 7 | it('mounts with soundcloud link', async () => { 8 | const { container } = render(SoundCloud, { 9 | soundcloudLink: 10 | 'https://soundcloud.com/mau5trap/deadmau5-bad-at-titles-episode-001', 11 | disable_observer: true, 12 | }); 13 | expect(container).toBeTruthy(); 14 | }); 15 | 16 | it('renders iframe with soundcloud link', async () => { 17 | const soundcloudLink = 18 | 'https://soundcloud.com/mau5trap/deadmau5-bad-at-titles-episode-001'; 19 | render(SoundCloud, { 20 | soundcloudLink, 21 | disable_observer: true, 22 | }); 23 | const iframe = page.getByTitle(`soundcloud-${soundcloudLink}`); 24 | const expected_src = `https://w.soundcloud.com/player/?url=${soundcloudLink}&visual=true`; 25 | await expect.element(iframe).toHaveAttribute('src', expected_src); 26 | }); 27 | 28 | it('mounts with custom height and width', async () => { 29 | const { container } = render(SoundCloud, { 30 | soundcloudLink: 31 | 'https://soundcloud.com/mau5trap/deadmau5-bad-at-titles-episode-001', 32 | height: '200px', 33 | width: '50%', 34 | disable_observer: true, 35 | }); 36 | const iframe = container.querySelector('iframe'); 37 | 38 | expect(iframe).toBeTruthy(); 39 | if (iframe) { 40 | expect(iframe.getAttribute('height')).toBe('200px'); 41 | expect(iframe.getAttribute('width')).toBe('50%'); 42 | } 43 | }); 44 | 45 | it('renders with a GeneralObserver', async () => { 46 | render(SoundCloud, { 47 | soundcloudLink: 48 | 'https://soundcloud.com/mau5trap/deadmau5-bad-at-titles-episode-001', 49 | disable_observer: false, 50 | }); 51 | const general_observer = page.getByTestId('general-observer'); 52 | await expect.element(general_observer).toBeInTheDocument(); 53 | }); 54 | 55 | // Coverage gaps - test stubs to implement 56 | it('should handle empty soundcloudLink gracefully', async () => { 57 | const { container } = render(SoundCloud, { 58 | soundcloudLink: '', 59 | disable_observer: true, 60 | }); 61 | const iframe = container.querySelector('iframe'); 62 | expect(iframe).toBeTruthy(); 63 | const src = iframe?.getAttribute('src'); 64 | expect(src).toBe( 65 | 'https://w.soundcloud.com/player/?url=&visual=true', 66 | ); 67 | }); 68 | 69 | it('should apply default height and width when not provided', async () => { 70 | const { container } = render(SoundCloud, { 71 | soundcloudLink: 'https://soundcloud.com/test/track', 72 | disable_observer: true, 73 | }); 74 | const iframe = container.querySelector('iframe'); 75 | expect(iframe).toBeTruthy(); 76 | 77 | expect(iframe?.getAttribute('width')).toBe('100%'); 78 | expect(iframe?.getAttribute('height')).toBe('300px'); 79 | expect(iframe?.getAttribute('scrolling')).toBe('false'); 80 | expect(iframe?.getAttribute('frameborder')).toBe('0'); 81 | expect(iframe?.getAttribute('allow')).toBe('autoplay'); 82 | }); 83 | 84 | it('should construct proper SoundCloud player URL', async () => { 85 | const soundcloudLink = 'https://soundcloud.com/artist/track-name'; 86 | const { container } = render(SoundCloud, { 87 | soundcloudLink, 88 | showVisual: false, 89 | disable_observer: true, 90 | }); 91 | const iframe = container.querySelector('iframe'); 92 | const src = iframe?.getAttribute('src'); 93 | 94 | expect(src).toBe( 95 | `https://w.soundcloud.com/player/?url=${soundcloudLink}&visual=false`, 96 | ); 97 | }); 98 | 99 | it('should handle special characters in soundcloudLink', async () => { 100 | const soundcloudLink = 101 | 'https://soundcloud.com/artist/track-with-symbols_123'; 102 | render(SoundCloud, { 103 | soundcloudLink, 104 | disable_observer: true, 105 | }); 106 | const iframe = page.getByTitle(`soundcloud-${soundcloudLink}`); 107 | const expected_src = `https://w.soundcloud.com/player/?url=${soundcloudLink}&visual=true`; 108 | await expect.element(iframe).toHaveAttribute('src', expected_src); 109 | }); 110 | 111 | it('should have proper iframe accessibility attributes', async () => { 112 | const soundcloudLink = 113 | 'https://soundcloud.com/test/accessibility'; 114 | const { container } = render(SoundCloud, { 115 | soundcloudLink, 116 | disable_observer: true, 117 | }); 118 | const iframe = container.querySelector('iframe'); 119 | 120 | expect(iframe?.getAttribute('title')).toBe( 121 | `soundcloud-${soundcloudLink}`, 122 | ); 123 | expect(iframe?.getAttribute('frameborder')).toBe('0'); 124 | expect(iframe?.getAttribute('scrolling')).toBe('false'); 125 | expect(iframe?.getAttribute('allow')).toBe('autoplay'); 126 | }); 127 | 128 | it('should handle very long soundcloudLink values', async () => { 129 | const soundcloudLink = 130 | 'https://soundcloud.com/artist/' + 'a'.repeat(100); 131 | const { container } = render(SoundCloud, { 132 | soundcloudLink, 133 | disable_observer: true, 134 | }); 135 | const iframe = container.querySelector('iframe'); 136 | const src = iframe?.getAttribute('src'); 137 | 138 | expect(src).toContain('https://w.soundcloud.com/player/?url='); 139 | expect(src).toContain(soundcloudLink); 140 | }); 141 | 142 | it('should apply visual parameter correctly', async () => { 143 | const soundcloudLink = 'https://soundcloud.com/test/track'; 144 | 145 | // Test visual=true 146 | const { container: visualContainer } = render(SoundCloud, { 147 | soundcloudLink, 148 | showVisual: true, 149 | disable_observer: true, 150 | }); 151 | const visualIframe = visualContainer.querySelector('iframe'); 152 | expect(visualIframe?.getAttribute('src')).toContain( 153 | 'visual=true', 154 | ); 155 | 156 | // Test visual=false 157 | const { container: noVisualContainer } = render(SoundCloud, { 158 | soundcloudLink, 159 | showVisual: false, 160 | disable_observer: true, 161 | }); 162 | const noVisualIframe = noVisualContainer.querySelector('iframe'); 163 | expect(noVisualIframe?.getAttribute('src')).toContain( 164 | 'visual=false', 165 | ); 166 | }); 167 | 168 | it('should handle numeric height and width values', async () => { 169 | const { container } = render(SoundCloud, { 170 | soundcloudLink: 'https://soundcloud.com/test/track', 171 | width: '500', 172 | height: '400', 173 | disable_observer: true, 174 | }); 175 | const iframe = container.querySelector('iframe'); 176 | 177 | expect(iframe?.getAttribute('width')).toBe('500'); 178 | expect(iframe?.getAttribute('height')).toBe('400'); 179 | }); 180 | 181 | it('should handle malformed SoundCloud links gracefully', async () => { 182 | const soundcloudLink = 'not-a-valid-url/with/problems'; 183 | const { container } = render(SoundCloud, { 184 | soundcloudLink, 185 | disable_observer: true, 186 | }); 187 | const iframe = container.querySelector('iframe'); 188 | 189 | // Component should still render, even if URL is malformed 190 | expect(iframe).toBeTruthy(); 191 | const src = iframe?.getAttribute('src'); 192 | expect(src).toContain(soundcloudLink); 193 | }); 194 | 195 | it('should render with proper CSS class structure', async () => { 196 | const { container } = render(SoundCloud, { 197 | soundcloudLink: 'https://soundcloud.com/test/track', 198 | iframe_styles: 'border: 1px solid red;', 199 | disable_observer: true, 200 | }); 201 | 202 | const iframe = container.querySelector('iframe'); 203 | expect(iframe?.style.cssText).toContain('border: 1px solid red;'); 204 | }); 205 | 206 | it('should handle different SoundCloud content types', async () => { 207 | // Test track URL 208 | const trackLink = 'https://soundcloud.com/artist/track-name'; 209 | const { container: trackContainer } = render(SoundCloud, { 210 | soundcloudLink: trackLink, 211 | disable_observer: true, 212 | }); 213 | const trackIframe = trackContainer.querySelector('iframe'); 214 | expect(trackIframe?.getAttribute('src')).toContain(trackLink); 215 | 216 | // Test playlist URL 217 | const playlistLink = 218 | 'https://soundcloud.com/artist/sets/playlist-name'; 219 | const { container: playlistContainer } = render(SoundCloud, { 220 | soundcloudLink: playlistLink, 221 | disable_observer: true, 222 | }); 223 | const playlistIframe = playlistContainer.querySelector('iframe'); 224 | expect(playlistIframe?.getAttribute('src')).toContain( 225 | playlistLink, 226 | ); 227 | 228 | // Test user profile URL 229 | const userLink = 'https://soundcloud.com/username'; 230 | const { container: userContainer } = render(SoundCloud, { 231 | soundcloudLink: userLink, 232 | disable_observer: true, 233 | }); 234 | const userIframe = userContainer.querySelector('iframe'); 235 | expect(userIframe?.getAttribute('src')).toContain(userLink); 236 | }); 237 | 238 | it('should handle SoundCloud URL variations', async () => { 239 | // Test short URL 240 | const shortLink = 'https://on.soundcloud.com/abc123'; 241 | const { container: shortContainer } = render(SoundCloud, { 242 | soundcloudLink: shortLink, 243 | disable_observer: true, 244 | }); 245 | const shortIframe = shortContainer.querySelector('iframe'); 246 | expect(shortIframe?.getAttribute('src')).toContain(shortLink); 247 | 248 | // Test URL with query parameters 249 | const urlWithParams = 'https://soundcloud.com/artist/track?t=30s'; 250 | const { container: paramsContainer } = render(SoundCloud, { 251 | soundcloudLink: urlWithParams, 252 | disable_observer: true, 253 | }); 254 | const paramsIframe = paramsContainer.querySelector('iframe'); 255 | expect(paramsIframe?.getAttribute('src')).toContain( 256 | urlWithParams, 257 | ); 258 | 259 | // Test mobile URL 260 | const mobileUrl = 'https://m.soundcloud.com/artist/track'; 261 | const { container: mobileContainer } = render(SoundCloud, { 262 | soundcloudLink: mobileUrl, 263 | disable_observer: true, 264 | }); 265 | const mobileIframe = mobileContainer.querySelector('iframe'); 266 | expect(mobileIframe?.getAttribute('src')).toContain(mobileUrl); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/spotify.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/spotify.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Spotify from '$lib/components/spotify.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | describe('Spotify', () => { 7 | it('mounts with spotify link', async () => { 8 | const { container } = render(Spotify, { 9 | spotifyLink: 'track/4uLU6hMCjMI75M1A2tKUQC', 10 | disable_observer: true, 11 | }); 12 | expect(container).toBeTruthy(); 13 | }); 14 | 15 | it('renders iframe with spotify link', async () => { 16 | const spotifyLink = 'track/4uLU6hMCjMI75M1A2tKUQC'; 17 | render(Spotify, { 18 | spotifyLink, 19 | disable_observer: true, 20 | }); 21 | const iframe = page.getByTestId('spotify'); 22 | const expected_src = `https://open.spotify.com/embed/${spotifyLink}`; 23 | await expect.element(iframe).toHaveAttribute('src', expected_src); 24 | }); 25 | 26 | it('renders with a GeneralObserver', async () => { 27 | render(Spotify, { 28 | spotifyLink: 'track/4uLU6hMCjMI75M1A2tKUQC', 29 | disable_observer: false, 30 | }); 31 | const general_observer = page.getByTestId('general-observer'); 32 | await expect.element(general_observer).toBeInTheDocument(); 33 | }); 34 | 35 | // Coverage gaps - test stubs to implement 36 | it('should handle empty spotifyLink gracefully', async () => { 37 | render(Spotify, { 38 | spotifyLink: '', 39 | disable_observer: true, 40 | }); 41 | const iframe = page.getByTestId('spotify'); 42 | const expected_src = 'https://open.spotify.com/embed/'; 43 | await expect.element(iframe).toHaveAttribute('src', expected_src); 44 | }); 45 | 46 | it('should apply default height and width when not provided', async () => { 47 | render(Spotify, { 48 | spotifyLink: 'track/test', 49 | disable_observer: true, 50 | }); 51 | const iframe = page.getByTestId('spotify'); 52 | const iframeElement = iframe.element() as HTMLIFrameElement; 53 | 54 | await expect 55 | .element(iframe) 56 | .toHaveAttribute('title', 'spotify-track/test'); 57 | await expect.element(iframe).toHaveAttribute('frameBorder', '0'); 58 | await expect 59 | .element(iframe) 60 | .toHaveAttribute('allow', 'encrypted-media'); 61 | expect(iframeElement.style.borderRadius).toContain('0.8rem'); 62 | }); 63 | 64 | it('should construct proper Spotify embed URL', async () => { 65 | const spotifyLink = 'playlist/37i9dQZF1DXcBWIGoYBM5M'; 66 | render(Spotify, { 67 | spotifyLink, 68 | disable_observer: true, 69 | }); 70 | const iframe = page.getByTestId('spotify'); 71 | const expected_src = `https://open.spotify.com/embed/${spotifyLink}`; 72 | await expect.element(iframe).toHaveAttribute('src', expected_src); 73 | }); 74 | 75 | it.skip('should handle different Spotify content types', async () => { 76 | // Test track 77 | const { getByTestId: getTrack } = render(Spotify, { 78 | spotifyLink: 'track/4uLU6hMCjMI75M1A2tKUQC', 79 | disable_observer: true, 80 | }); 81 | const trackIframe = getTrack('spotify'); 82 | await expect 83 | .element(trackIframe) 84 | .toHaveAttribute( 85 | 'src', 86 | 'https://open.spotify.com/embed/track/4uLU6hMCjMI75M1A2tKUQC', 87 | ); 88 | 89 | // Test album 90 | const { getByTestId: getAlbum } = render(Spotify, { 91 | spotifyLink: 'album/1DFixLWuPkv3KT3TnV35m3', 92 | disable_observer: true, 93 | }); 94 | const albumIframe = getAlbum('spotify'); 95 | await expect 96 | .element(albumIframe) 97 | .toHaveAttribute( 98 | 'src', 99 | 'https://open.spotify.com/embed/album/1DFixLWuPkv3KT3TnV35m3', 100 | ); 101 | 102 | // Test playlist 103 | const { getByTestId: getPlaylist } = render(Spotify, { 104 | spotifyLink: 'playlist/37i9dQZF1DXcBWIGoYBM5M', 105 | disable_observer: true, 106 | }); 107 | const playlistIframe = getPlaylist('spotify'); 108 | await expect 109 | .element(playlistIframe) 110 | .toHaveAttribute( 111 | 'src', 112 | 'https://open.spotify.com/embed/playlist/37i9dQZF1DXcBWIGoYBM5M', 113 | ); 114 | 115 | // Test artist 116 | const { getByTestId: getArtist } = render(Spotify, { 117 | spotifyLink: 'artist/4NHQUGzhtTLFvgF5SZesLK', 118 | disable_observer: true, 119 | }); 120 | const artistIframe = getArtist('spotify'); 121 | await expect 122 | .element(artistIframe) 123 | .toHaveAttribute( 124 | 'src', 125 | 'https://open.spotify.com/embed/artist/4NHQUGzhtTLFvgF5SZesLK', 126 | ); 127 | }); 128 | 129 | it('should handle special characters in spotifyLink', async () => { 130 | const spotifyLink = 'track/abc123_def-456'; 131 | render(Spotify, { 132 | spotifyLink, 133 | disable_observer: true, 134 | }); 135 | const iframe = page.getByTestId('spotify'); 136 | await expect 137 | .element(iframe) 138 | .toHaveAttribute( 139 | 'src', 140 | `https://open.spotify.com/embed/${spotifyLink}`, 141 | ); 142 | await expect 143 | .element(iframe) 144 | .toHaveAttribute('title', `spotify-${spotifyLink}`); 145 | }); 146 | 147 | it('should have proper iframe accessibility attributes', async () => { 148 | const spotifyLink = 'track/accessibility-test'; 149 | render(Spotify, { 150 | spotifyLink, 151 | disable_observer: true, 152 | }); 153 | const iframe = page.getByTestId('spotify'); 154 | 155 | await expect 156 | .element(iframe) 157 | .toHaveAttribute('title', `spotify-${spotifyLink}`); 158 | await expect.element(iframe).toHaveAttribute('frameBorder', '0'); 159 | await expect 160 | .element(iframe) 161 | .toHaveAttribute('allow', 'encrypted-media'); 162 | await expect 163 | .element(iframe) 164 | .toHaveClass('spotify-sveltekit-embed'); 165 | }); 166 | 167 | it('should handle very long spotifyLink values', async () => { 168 | const spotifyLink = 'track/' + 'a'.repeat(100); 169 | render(Spotify, { 170 | spotifyLink, 171 | disable_observer: true, 172 | }); 173 | const iframe = page.getByTestId('spotify'); 174 | await expect 175 | .element(iframe) 176 | .toHaveAttribute( 177 | 'src', 178 | `https://open.spotify.com/embed/${spotifyLink}`, 179 | ); 180 | }); 181 | 182 | it('should apply custom styles correctly', async () => { 183 | const customStyles = 'border: 2px solid red; background: blue;'; 184 | render(Spotify, { 185 | spotifyLink: 'track/test', 186 | iframe_styles: customStyles, 187 | disable_observer: true, 188 | }); 189 | const iframe = page.getByTestId('spotify'); 190 | const iframeElement = iframe.element() as HTMLIFrameElement; 191 | 192 | expect(iframeElement.style.cssText).toContain( 193 | 'border: 2px solid red', 194 | ); 195 | expect(iframeElement.style.cssText).toContain('background: blue'); 196 | }); 197 | 198 | it('should handle custom dimensions', async () => { 199 | render(Spotify, { 200 | spotifyLink: 'track/test', 201 | width: '500px', 202 | height: '300px', 203 | disable_observer: true, 204 | }); 205 | const iframe = page.getByTestId('spotify'); 206 | const iframeElement = iframe.element() as HTMLIFrameElement; 207 | 208 | // Default iframe_styles should incorporate custom dimensions 209 | expect(iframeElement.style.cssText).toContain('height: 300px'); 210 | expect(iframeElement.style.cssText).toContain('width: 500px'); 211 | }); 212 | 213 | it('should handle malformed Spotify links gracefully', async () => { 214 | const spotifyLink = 'invalid/content/with/extra/slashes'; 215 | render(Spotify, { 216 | spotifyLink, 217 | disable_observer: true, 218 | }); 219 | const iframe = page.getByTestId('spotify'); 220 | 221 | // Component should still render, even if link is malformed 222 | await expect 223 | .element(iframe) 224 | .toHaveAttribute( 225 | 'src', 226 | `https://open.spotify.com/embed/${spotifyLink}`, 227 | ); 228 | }); 229 | 230 | it('should render with proper CSS class structure', async () => { 231 | render(Spotify, { 232 | spotifyLink: 'track/test', 233 | disable_observer: true, 234 | }); 235 | const iframe = page.getByTestId('spotify'); 236 | 237 | await expect 238 | .element(iframe) 239 | .toHaveClass('spotify-sveltekit-embed'); 240 | }); 241 | 242 | it.skip('should handle Spotify link variations', async () => { 243 | // Test with query parameters 244 | const linkWithParams = 'track/4uLU6hMCjMI75M1A2tKUQC?si=abc123'; 245 | const { getByTestId: getWithParams } = render(Spotify, { 246 | spotifyLink: linkWithParams, 247 | disable_observer: true, 248 | }); 249 | const paramsIframe = getWithParams('spotify'); 250 | await expect 251 | .element(paramsIframe) 252 | .toHaveAttribute( 253 | 'src', 254 | `https://open.spotify.com/embed/${linkWithParams}`, 255 | ); 256 | 257 | // Test with just ID (no type prefix) 258 | const justId = '4uLU6hMCjMI75M1A2tKUQC'; 259 | const { getByTestId: getJustId } = render(Spotify, { 260 | spotifyLink: justId, 261 | disable_observer: true, 262 | }); 263 | const idIframe = getJustId('spotify'); 264 | await expect 265 | .element(idIframe) 266 | .toHaveAttribute( 267 | 'src', 268 | `https://open.spotify.com/embed/${justId}`, 269 | ); 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/stackblitz.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 | 65 | 66 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/tiktok.svelte: -------------------------------------------------------------------------------- 1 | 62 | 63 | 64 |
72 | 87 |
88 |
89 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/tiktok.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import TikTok from '$lib/components/tiktok.svelte'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { render } from 'vitest-browser-svelte'; 4 | import { page } from '@vitest/browser/context'; 5 | 6 | describe('TikTok', () => { 7 | it('mounts with default props', async () => { 8 | const { container } = render(TikTok, { 9 | tiktokId: '7234660647688875814', 10 | disable_observer: true, 11 | }); 12 | expect(container).toBeTruthy(); 13 | }); 14 | 15 | it('renders iframe with correct src', async () => { 16 | const tiktokId = '7234660647688875814'; 17 | render(TikTok, { 18 | tiktokId, 19 | disable_observer: true, 20 | }); 21 | const iframe = page.getByTestId('tiktok-embed'); 22 | const src = iframe.element().getAttribute('src'); 23 | 24 | expect(src).toContain( 25 | `https://www.tiktok.com/player/v1/${tiktokId}?`, 26 | ); 27 | expect(src).toContain('controls=1'); 28 | expect(src).toContain('progress_bar=1'); 29 | }); 30 | 31 | it('renders with custom configurations', async () => { 32 | const tiktokId = '7234660647688875814'; 33 | render(TikTok, { 34 | tiktokId, 35 | controls: false, 36 | autoplay: true, 37 | loop: true, 38 | music_info: true, 39 | description: true, 40 | disable_observer: true, 41 | }); 42 | const iframe = page.getByTestId('tiktok-embed'); 43 | const src = iframe.element().getAttribute('src'); 44 | 45 | expect(src).toContain('controls=0'); 46 | expect(src).toContain('autoplay=1'); 47 | expect(src).toContain('loop=1'); 48 | expect(src).toContain('music_info=1'); 49 | expect(src).toContain('description=1'); 50 | }); 51 | 52 | it('renders with a GeneralObserver', async () => { 53 | render(TikTok, { 54 | tiktokId: '7234660647688875814', 55 | disable_observer: false, 56 | }); 57 | const general_observer = page.getByTestId('general-observer'); 58 | await expect.element(general_observer).toBeInTheDocument(); 59 | }); 60 | 61 | // Coverage gaps - test stubs to implement 62 | it('should handle empty tiktokId gracefully', async () => { 63 | render(TikTok, { 64 | tiktokId: '', 65 | disable_observer: true, 66 | }); 67 | const iframe = page.getByTestId('tiktok-embed'); 68 | const src = iframe.element().getAttribute('src'); 69 | expect(src).toContain('https://www.tiktok.com/player/v1/?'); 70 | }); 71 | 72 | it('should apply default prop values when not provided', async () => { 73 | const { getByTestId, container } = render(TikTok, { 74 | tiktokId: '7234660647688875814', 75 | disable_observer: true, 76 | }); 77 | const iframe = page.getByTestId('tiktok-embed'); 78 | 79 | await expect.element(iframe).toHaveAttribute('frameborder', '0'); 80 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 81 | await expect 82 | .element(iframe) 83 | .toHaveAttribute( 84 | 'allow', 85 | 'encrypted-media; picture-in-picture; fullscreen', 86 | ); 87 | 88 | const embedDiv = container.querySelector( 89 | '.tiktok-sveltekit-embed', 90 | ); 91 | expect(embedDiv).toBeTruthy(); 92 | const styles = (embedDiv as HTMLElement)?.style; 93 | expect(styles?.width).toBe('100%'); 94 | expect(styles?.height).toBe('600px'); 95 | }); 96 | 97 | it('should construct proper TikTok player URL', async () => { 98 | const tiktokId = '1234567890123456789'; 99 | render(TikTok, { 100 | tiktokId, 101 | controls: true, 102 | autoplay: false, 103 | disable_observer: true, 104 | }); 105 | const iframe = page.getByTestId('tiktok-embed'); 106 | const src = iframe.element().getAttribute('src'); 107 | 108 | expect(src).toContain( 109 | `https://www.tiktok.com/player/v1/${tiktokId}?`, 110 | ); 111 | expect(src).toContain('controls=1'); 112 | expect(src).toContain('autoplay=0'); 113 | }); 114 | 115 | it('should handle special characters in tiktokId', async () => { 116 | const tiktokId = 'abc123_def-456'; 117 | render(TikTok, { 118 | tiktokId, 119 | disable_observer: true, 120 | }); 121 | const iframe = page.getByTestId('tiktok-embed'); 122 | 123 | await expect 124 | .element(iframe) 125 | .toHaveAttribute('title', `tiktok-${tiktokId}`); 126 | const src = iframe.element().getAttribute('src'); 127 | expect(src).toContain(tiktokId); 128 | }); 129 | 130 | it('should have proper iframe accessibility attributes', async () => { 131 | const tiktokId = 'accessibility-test'; 132 | render(TikTok, { 133 | tiktokId, 134 | disable_observer: true, 135 | }); 136 | const iframe = page.getByTestId('tiktok-embed'); 137 | 138 | await expect 139 | .element(iframe) 140 | .toHaveAttribute('title', `tiktok-${tiktokId}`); 141 | await expect.element(iframe).toHaveAttribute('frameborder', '0'); 142 | await expect.element(iframe).toHaveAttribute('scrolling', 'no'); 143 | await expect 144 | .element(iframe) 145 | .toHaveAttribute( 146 | 'allow', 147 | 'encrypted-media; picture-in-picture; fullscreen', 148 | ); 149 | }); 150 | 151 | it('should handle very long tiktokId values', async () => { 152 | const tiktokId = '1'.repeat(50); 153 | render(TikTok, { 154 | tiktokId, 155 | disable_observer: true, 156 | }); 157 | const iframe = page.getByTestId('tiktok-embed'); 158 | const src = iframe.element().getAttribute('src'); 159 | 160 | expect(src).toContain(tiktokId); 161 | }); 162 | 163 | it('should apply custom dimensions correctly', async () => { 164 | const { container } = render(TikTok, { 165 | tiktokId: '7234660647688875814', 166 | width: '500px', 167 | height: '400px', 168 | disable_observer: true, 169 | }); 170 | 171 | const embedDiv = container.querySelector( 172 | '.tiktok-sveltekit-embed', 173 | ); 174 | expect(embedDiv).toBeTruthy(); 175 | const styles = (embedDiv as HTMLElement)?.style; 176 | expect(styles?.width).toBe('500px'); 177 | expect(styles?.height).toBe('400px'); 178 | }); 179 | 180 | it('should handle boolean control options correctly', async () => { 181 | render(TikTok, { 182 | tiktokId: '7234660647688875814', 183 | controls: false, 184 | progress_bar: false, 185 | play_button: false, 186 | volume_control: false, 187 | fullscreen_button: false, 188 | timestamp: false, 189 | loop: true, 190 | autoplay: true, 191 | music_info: true, 192 | description: true, 193 | rel: false, 194 | native_context_menu: false, 195 | closed_caption: false, 196 | disable_observer: true, 197 | }); 198 | const iframe = page.getByTestId('tiktok-embed'); 199 | const src = iframe.element().getAttribute('src'); 200 | 201 | expect(src).toContain('controls=0'); 202 | expect(src).toContain('progress_bar=0'); 203 | expect(src).toContain('play_button=0'); 204 | expect(src).toContain('volume_control=0'); 205 | expect(src).toContain('fullscreen_button=0'); 206 | expect(src).toContain('timestamp=0'); 207 | expect(src).toContain('loop=1'); 208 | expect(src).toContain('autoplay=1'); 209 | expect(src).toContain('music_info=1'); 210 | expect(src).toContain('description=1'); 211 | expect(src).toContain('rel=0'); 212 | expect(src).toContain('native_context_menu=0'); 213 | expect(src).toContain('closed_caption=0'); 214 | }); 215 | 216 | it('should handle malformed tiktokId gracefully', async () => { 217 | const tiktokId = 'invalid/id/with/slashes'; 218 | render(TikTok, { 219 | tiktokId, 220 | disable_observer: true, 221 | }); 222 | const iframe = page.getByTestId('tiktok-embed'); 223 | 224 | // Component should still render, even if ID is malformed 225 | const src = iframe.element().getAttribute('src'); 226 | expect(src).toContain(tiktokId); 227 | }); 228 | 229 | it('should render with proper CSS class structure', async () => { 230 | const { container } = render(TikTok, { 231 | tiktokId: '7234660647688875814', 232 | disable_observer: true, 233 | }); 234 | 235 | const embedDiv = container.querySelector( 236 | '.tiktok-sveltekit-embed', 237 | ); 238 | expect(embedDiv).toBeTruthy(); 239 | expect(embedDiv?.className).toBe('tiktok-sveltekit-embed'); 240 | }); 241 | 242 | it.skip('should handle different TikTok content formats', async () => { 243 | // Test standard video ID 244 | const { getByTestId: getStandard } = render(TikTok, { 245 | tiktokId: '7234660647688875814', 246 | disable_observer: true, 247 | }); 248 | const standardIframe = getStandard('tiktok-embed'); 249 | expect(standardIframe.element().getAttribute('src')).toContain( 250 | '7234660647688875814', 251 | ); 252 | 253 | // Test different ID format 254 | const { getByTestId: getDifferent } = render(TikTok, { 255 | tiktokId: '6982674499405171973', 256 | disable_observer: true, 257 | }); 258 | const differentIframe = getDifferent('tiktok-embed'); 259 | expect(differentIframe.element().getAttribute('src')).toContain( 260 | '6982674499405171973', 261 | ); 262 | }); 263 | 264 | it.skip('should handle query parameter construction correctly', async () => { 265 | render(TikTok, { 266 | tiktokId: '7234660647688875814', 267 | controls: true, 268 | progress_bar: false, 269 | autoplay: true, 270 | loop: false, 271 | disable_observer: true, 272 | }); 273 | const iframe = page.getByTestId('tiktok-embed'); 274 | const src = iframe.element().getAttribute('src'); 275 | 276 | // Check that all parameters are included with correct values 277 | expect(src).toContain('controls=1'); 278 | expect(src).toContain('progress_bar=0'); 279 | expect(src).toContain('autoplay=1'); 280 | expect(src).toContain('loop=0'); 281 | 282 | // Check that URL is properly formatted 283 | expect(src).toMatch( 284 | /^https:\/\/www\.tiktok\.com\/player\/v1\/7234660647688875814\?.*controls=1.*progress_bar=0.*autoplay=1.*loop=0/, 285 | ); 286 | }); 287 | 288 | it('should apply iframe positioning styles correctly', async () => { 289 | render(TikTok, { 290 | tiktokId: '7234660647688875814', 291 | disable_observer: true, 292 | }); 293 | const iframe = page.getByTestId('tiktok-embed'); 294 | const iframeElement = iframe.element() as HTMLIFrameElement; 295 | const styles = iframeElement.style; 296 | 297 | expect(styles.position).toBe('absolute'); 298 | expect(styles.top).toBe('0px'); 299 | expect(styles.left).toBe('0px'); 300 | expect(styles.width).toBe('100%'); 301 | expect(styles.height).toBe('100%'); 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/toot.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 | 50 |
51 | 52 | 64 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/tweet.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
50 | 53 |
54 | 55 | 80 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/tweet.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Tweet from '$lib/components/tweet.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it, vi } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | describe('Tweet', () => { 7 | it('mounts with default values', async () => { 8 | const { container } = render(Tweet, { 9 | tweetLink: 'twitterdev/status/1399879412994844160', 10 | }); 11 | 12 | expect(container).toBeTruthy(); 13 | }); 14 | 15 | it('renders tweet with correct link', async () => { 16 | const tweetLink = 'twitterdev/status/1399879412994844160'; 17 | 18 | render(Tweet, { 19 | tweetLink, 20 | }); 21 | const tweetElement = page.getByText('Loading Tweet...'); 22 | 23 | await expect 24 | .element(tweetElement) 25 | .toHaveAttribute('href', `https://twitter.com/${tweetLink}`); 26 | }); 27 | 28 | // Coverage gaps - test stubs to implement 29 | it('should handle empty tweetLink gracefully', async () => { 30 | const tweetLink = ''; 31 | 32 | render(Tweet, { 33 | tweetLink, 34 | }); 35 | const tweetElement = page.getByText('Loading Tweet...'); 36 | 37 | await expect 38 | .element(tweetElement) 39 | .toHaveAttribute('href', 'https://twitter.com/'); 40 | }); 41 | 42 | it('should apply default prop values when not provided', async () => { 43 | const { container } = render(Tweet, { 44 | tweetLink: 'twitterdev/status/1399879412994844160', 45 | }); 46 | 47 | const wrapper = container.querySelector('.tweet-wrapper'); 48 | const blockquote = container.querySelector('.twitter-tweet'); 49 | 50 | expect(wrapper).toBeTruthy(); 51 | expect(blockquote).toBeTruthy(); 52 | 53 | if (wrapper && blockquote) { 54 | await expect 55 | .element(wrapper) 56 | .toHaveAttribute('data-theme', 'light'); 57 | await expect 58 | .element(blockquote) 59 | .toHaveAttribute('data-theme', 'light'); 60 | } 61 | }); 62 | 63 | it('should handle special characters in tweetLink', async () => { 64 | const tweetLink = 'user.name/status/123_456'; 65 | 66 | render(Tweet, { 67 | tweetLink, 68 | }); 69 | const tweetElement = page.getByText('Loading Tweet...'); 70 | 71 | await expect 72 | .element(tweetElement) 73 | .toHaveAttribute('href', `https://twitter.com/${tweetLink}`); 74 | }); 75 | 76 | it('should construct proper Twitter embed URL', async () => { 77 | const tweetLink = 'username/status/123456789'; 78 | 79 | render(Tweet, { 80 | tweetLink, 81 | }); 82 | const tweetElement = page.getByText('Loading Tweet...'); 83 | 84 | const expectedHref = `https://twitter.com/${tweetLink}`; 85 | await expect 86 | .element(tweetElement) 87 | .toHaveAttribute('href', expectedHref); 88 | }); 89 | 90 | it('should handle very long tweetLink values', async () => { 91 | const tweetLink = `${'a'.repeat(50)}/status/${'1'.repeat(20)}`; 92 | 93 | render(Tweet, { 94 | tweetLink, 95 | }); 96 | const tweetElement = page.getByText('Loading Tweet...'); 97 | 98 | await expect 99 | .element(tweetElement) 100 | .toHaveAttribute('href', `https://twitter.com/${tweetLink}`); 101 | }); 102 | 103 | it('should apply custom CSS styles correctly', async () => { 104 | const { container } = render(Tweet, { 105 | tweetLink: 'twitterdev/status/1399879412994844160', 106 | theme: 'dark', 107 | }); 108 | 109 | const wrapper = container.querySelector('.tweet-wrapper'); 110 | const blockquote = container.querySelector('.twitter-tweet'); 111 | 112 | expect(wrapper).toBeTruthy(); 113 | expect(blockquote).toBeTruthy(); 114 | 115 | if (wrapper && blockquote) { 116 | const wrapperElement = wrapper as HTMLDivElement; 117 | const blockquoteElement = blockquote as HTMLElement; 118 | 119 | // Check computed styles 120 | const wrapperStyles = window.getComputedStyle(wrapperElement); 121 | expect(wrapperStyles.display).toBe('flex'); 122 | expect(wrapperStyles.justifyContent).toBe('center'); 123 | 124 | await expect 125 | .element(wrapper) 126 | .toHaveAttribute('data-theme', 'dark'); 127 | await expect 128 | .element(blockquote) 129 | .toHaveAttribute('data-theme', 'dark'); 130 | } 131 | }); 132 | 133 | it('should handle malformed tweet links gracefully', async () => { 134 | const tweetLink = 'invalid/format/link'; 135 | 136 | render(Tweet, { 137 | tweetLink, 138 | }); 139 | const tweetElement = page.getByText('Loading Tweet...'); 140 | 141 | await expect 142 | .element(tweetElement) 143 | .toHaveAttribute('href', `https://twitter.com/${tweetLink}`); 144 | }); 145 | 146 | it('should render loading state properly', async () => { 147 | render(Tweet, { 148 | tweetLink: 'twitterdev/status/1399879412994844160', 149 | }); 150 | 151 | const loadingText = page.getByText('Loading Tweet...'); 152 | await expect.element(loadingText).toBeInTheDocument(); 153 | }); 154 | 155 | it.skip('should handle Twitter script loading', async () => { 156 | const mockScript = document.createElement('script'); 157 | const createElementSpy = vi 158 | .spyOn(document, 'createElement') 159 | .mockReturnValue(mockScript); 160 | const appendChildSpy = vi.spyOn(document.head, 'appendChild'); 161 | 162 | render(Tweet, { 163 | tweetLink: 'twitterdev/status/1399879412994844160', 164 | }); 165 | 166 | expect(createElementSpy).toHaveBeenCalledWith('script'); 167 | expect(mockScript.src).toBe( 168 | 'https://platform.twitter.com/widgets.js', 169 | ); 170 | expect(mockScript.async).toBe(true); 171 | expect(appendChildSpy).toHaveBeenCalledWith(mockScript); 172 | 173 | createElementSpy.mockRestore(); 174 | appendChildSpy.mockRestore(); 175 | }); 176 | 177 | it.skip('should handle component unmount and cleanup', async () => { 178 | const mockScript = document.createElement('script'); 179 | const createElementSpy = vi 180 | .spyOn(document, 'createElement') 181 | .mockReturnValue(mockScript); 182 | const appendChildSpy = vi.spyOn(document.head, 'appendChild'); 183 | const removeChildSpy = vi.spyOn(document.head, 'removeChild'); 184 | 185 | const { unmount } = render(Tweet, { 186 | tweetLink: 'twitterdev/status/1399879412994844160', 187 | }); 188 | 189 | // Unmount component 190 | unmount(); 191 | 192 | expect(removeChildSpy).toHaveBeenCalledWith(mockScript); 193 | 194 | createElementSpy.mockRestore(); 195 | appendChildSpy.mockRestore(); 196 | removeChildSpy.mockRestore(); 197 | }); 198 | 199 | it.skip('should have proper accessibility attributes', async () => { 200 | const { container } = render(Tweet, { 201 | tweetLink: 'twitterdev/status/1399879412994844160', 202 | }); 203 | 204 | const link = container.querySelector('a'); 205 | expect(link).toBeTruthy(); 206 | 207 | if (link) { 208 | const linkElement = link as HTMLAnchorElement; 209 | expect(linkElement.textContent).toBe('Loading Tweet...'); 210 | expect(linkElement.style.color).toBe('rgb(29, 161, 242)'); // Twitter blue 211 | expect(linkElement.style.fontWeight).toBe('bold'); 212 | expect(linkElement.style.textDecoration).toBe('none'); 213 | } 214 | }); 215 | 216 | it.skip('should handle different tweet link formats', async () => { 217 | const testCases = [ 218 | 'username/status/123456789', 219 | 'user.name/status/987654321', 220 | 'test_user/status/111222333', 221 | 'user123/status/456789012', 222 | ]; 223 | 224 | for (const tweetLink of testCases) { 225 | render(Tweet, { 226 | tweetLink, 227 | }); 228 | const tweetElement = page.getByText('Loading Tweet...'); 229 | 230 | await expect 231 | .element(tweetElement) 232 | .toHaveAttribute('href', `https://twitter.com/${tweetLink}`); 233 | } 234 | }); 235 | 236 | it.skip('should not load duplicate Twitter scripts', async () => { 237 | // Mock existing script detection 238 | const existingScript = document.createElement('script'); 239 | existingScript.src = 'https://platform.twitter.com/widgets.js'; 240 | document.head.appendChild(existingScript); 241 | 242 | const createElementSpy = vi.spyOn(document, 'createElement'); 243 | const appendChildSpy = vi.spyOn(document.head, 'appendChild'); 244 | 245 | render(Tweet, { 246 | tweetLink: 'twitterdev/status/1399879412994844160', 247 | }); 248 | 249 | // Should not create a new script since one already exists 250 | expect(createElementSpy).not.toHaveBeenCalled(); 251 | expect(appendChildSpy).not.toHaveBeenCalled(); 252 | 253 | // Cleanup 254 | document.head.removeChild(existingScript); 255 | createElementSpy.mockRestore(); 256 | appendChildSpy.mockRestore(); 257 | }); 258 | 259 | it('should handle theme prop correctly', async () => { 260 | const { container } = render(Tweet, { 261 | tweetLink: 'twitterdev/status/1399879412994844160', 262 | theme: 'dark', 263 | }); 264 | 265 | const wrapper = container.querySelector('.tweet-wrapper'); 266 | const blockquote = container.querySelector('.twitter-tweet'); 267 | 268 | expect(wrapper).toBeTruthy(); 269 | expect(blockquote).toBeTruthy(); 270 | 271 | if (wrapper && blockquote) { 272 | await expect 273 | .element(wrapper) 274 | .toHaveAttribute('data-theme', 'dark'); 275 | await expect 276 | .element(blockquote) 277 | .toHaveAttribute('data-theme', 'dark'); 278 | } 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/vimeo.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 |
34 | 48 |
49 |
50 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/vimeo.svelte.test.ts: -------------------------------------------------------------------------------- 1 | import Vimeo from '$lib/components/vimeo.svelte'; 2 | import { page } from '@vitest/browser/context'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-svelte'; 5 | 6 | describe('Vimeo', () => { 7 | it('mounts with default values', async () => { 8 | const { container } = render(Vimeo, { 9 | vimeoId: '123456789', 10 | disable_observer: true, 11 | }); 12 | 13 | expect(container).toBeTruthy(); 14 | }); 15 | 16 | it('renders iframe with correct src', async () => { 17 | const vimeoId = '987654321'; 18 | render(Vimeo, { 19 | vimeoId, 20 | autoPlay: true, 21 | aspectRatio: '4:3', 22 | skipTo: { h: 1, m: 23, s: 45 }, 23 | disable_observer: true, 24 | }); 25 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 26 | 27 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=true#t=1h23m45s`; 28 | await expect.element(iframe).toHaveAttribute('src', expected_src); 29 | }); 30 | 31 | it('mounts with custom aspect ratio', async () => { 32 | const { container } = render(Vimeo, { 33 | vimeoId: '123456789', 34 | aspectRatio: '1:1', 35 | disable_observer: true, 36 | }); 37 | const wrapper = container.querySelector('.vimeo-svelte-embed'); 38 | 39 | expect(wrapper?.getAttribute('style')).toContain( 40 | 'padding-top: 100%;', 41 | ); 42 | }); 43 | 44 | it('renders with a GeneralObserver', async () => { 45 | render(Vimeo, { 46 | vimeoId: '123456789', 47 | disable_observer: false, 48 | }); 49 | const general_observer = page.getByTestId('general-observer'); 50 | 51 | await expect.element(general_observer).toBeInTheDocument(); 52 | }); 53 | 54 | // Coverage gaps - test stubs to implement 55 | it('should handle empty vimeoId gracefully', async () => { 56 | const vimeoId = ''; 57 | 58 | render(Vimeo, { 59 | vimeoId, 60 | disable_observer: true, 61 | }); 62 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 63 | 64 | const expected_src = `https://player.vimeo.com/video/?autoplay=false#t=0h0m0s`; 65 | await expect.element(iframe).toHaveAttribute('src', expected_src); 66 | }); 67 | 68 | it('should apply default prop values when not provided', async () => { 69 | const vimeoId = '123456789'; 70 | 71 | const { getByTitle, container } = render(Vimeo, { 72 | vimeoId, 73 | disable_observer: true, 74 | }); 75 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 76 | const wrapper = container.querySelector('.vimeo-svelte-embed'); 77 | 78 | // Check default values 79 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 80 | await expect.element(iframe).toHaveAttribute('src', expected_src); 81 | 82 | // Check default aspect ratio (16:9 = 56.25%) 83 | expect(wrapper?.getAttribute('style')).toContain( 84 | 'padding-top: 56.25%;', 85 | ); 86 | }); 87 | 88 | it('should handle different aspect ratio formats', async () => { 89 | const aspectRatios = ['16:9', '4:3', '1:1', '3:2', '8.5']; 90 | const expectedPaddings = [ 91 | '56.25%', 92 | '75%', 93 | '100%', 94 | '66.66%', 95 | '62.5%', 96 | ]; 97 | 98 | for (let i = 0; i < aspectRatios.length; i++) { 99 | const { container } = render(Vimeo, { 100 | vimeoId: '123456789', 101 | aspectRatio: aspectRatios[i], 102 | disable_observer: true, 103 | }); 104 | const wrapper = container.querySelector('.vimeo-svelte-embed'); 105 | 106 | expect(wrapper?.getAttribute('style')).toContain( 107 | `padding-top: ${expectedPaddings[i]};`, 108 | ); 109 | } 110 | }); 111 | 112 | it('should construct proper Vimeo player URL', async () => { 113 | const vimeoId = '987654321'; 114 | 115 | render(Vimeo, { 116 | vimeoId, 117 | autoPlay: false, 118 | disable_observer: true, 119 | }); 120 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 121 | 122 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 123 | await expect.element(iframe).toHaveAttribute('src', expected_src); 124 | }); 125 | 126 | it('should handle skipTo time parameter correctly', async () => { 127 | const vimeoId = '123456789'; 128 | const skipTo = { h: 2, m: 15, s: 30 }; 129 | 130 | render(Vimeo, { 131 | vimeoId, 132 | skipTo, 133 | disable_observer: true, 134 | }); 135 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 136 | 137 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=2h15m30s`; 138 | await expect.element(iframe).toHaveAttribute('src', expected_src); 139 | }); 140 | 141 | it.skip('should handle autoPlay parameter in URL', async () => { 142 | const vimeoId = '123456789'; 143 | 144 | // Test autoPlay true 145 | const { getByTitle: getByTitleTrue } = render(Vimeo, { 146 | vimeoId, 147 | autoPlay: true, 148 | disable_observer: true, 149 | }); 150 | const iframeTrue = getByTitleTrue(`vimeo-${vimeoId}`); 151 | 152 | const expected_src_true = `https://player.vimeo.com/video/${vimeoId}?autoplay=true#t=0h0m0s`; 153 | await expect 154 | .element(iframeTrue) 155 | .toHaveAttribute('src', expected_src_true); 156 | 157 | // Test autoPlay false 158 | const { getByTitle: getByTitleFalse } = render(Vimeo, { 159 | vimeoId, 160 | autoPlay: false, 161 | disable_observer: true, 162 | }); 163 | const iframeFalse = getByTitleFalse(`vimeo-${vimeoId}`); 164 | 165 | const expected_src_false = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 166 | await expect 167 | .element(iframeFalse) 168 | .toHaveAttribute('src', expected_src_false); 169 | }); 170 | 171 | it('should handle special characters in vimeoId', async () => { 172 | const vimeoId = '123456789'; 173 | 174 | render(Vimeo, { 175 | vimeoId, 176 | disable_observer: true, 177 | }); 178 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 179 | 180 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 181 | await expect.element(iframe).toHaveAttribute('src', expected_src); 182 | }); 183 | 184 | it('should have proper iframe accessibility attributes', async () => { 185 | const vimeoId = '123456789'; 186 | 187 | const { container } = render(Vimeo, { 188 | vimeoId, 189 | disable_observer: true, 190 | }); 191 | const iframe = container.querySelector('iframe'); 192 | 193 | expect(iframe).toBeTruthy(); 194 | if (iframe) { 195 | const iframeElement = iframe as HTMLIFrameElement; 196 | await expect 197 | .element(iframe) 198 | .toHaveAttribute('title', `vimeo-${vimeoId}`); 199 | await expect 200 | .element(iframe) 201 | .toHaveAttribute('frameBorder', '0'); 202 | await expect 203 | .element(iframe) 204 | .toHaveAttribute( 205 | 'allow', 206 | 'autoplay; fullscreen; picture-in-picture', 207 | ); 208 | await expect.element(iframe).toHaveAttribute('allowFullScreen'); 209 | } 210 | }); 211 | 212 | it('should handle very long vimeoId values', async () => { 213 | const vimeoId = '1'.repeat(20); 214 | 215 | render(Vimeo, { 216 | vimeoId, 217 | disable_observer: true, 218 | }); 219 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 220 | 221 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 222 | await expect.element(iframe).toHaveAttribute('src', expected_src); 223 | }); 224 | 225 | it('should calculate aspect ratio padding correctly', async () => { 226 | const testCases = [ 227 | { ratio: '16:9', expected: '56.25%' }, 228 | { ratio: '4:3', expected: '75%' }, 229 | { ratio: '1:1', expected: '100%' }, 230 | { ratio: '3:2', expected: '66.66%' }, 231 | { ratio: '8.5', expected: '62.5%' }, 232 | ]; 233 | 234 | for (const testCase of testCases) { 235 | const { container } = render(Vimeo, { 236 | vimeoId: '123456789', 237 | aspectRatio: testCase.ratio, 238 | disable_observer: true, 239 | }); 240 | const wrapper = container.querySelector('.vimeo-svelte-embed'); 241 | 242 | expect(wrapper?.getAttribute('style')).toContain( 243 | `padding-top: ${testCase.expected};`, 244 | ); 245 | } 246 | }); 247 | 248 | it('should handle malformed vimeo IDs gracefully', async () => { 249 | const vimeoId = 'abc123def'; 250 | 251 | render(Vimeo, { 252 | vimeoId, 253 | disable_observer: true, 254 | }); 255 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 256 | 257 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 258 | await expect.element(iframe).toHaveAttribute('src', expected_src); 259 | }); 260 | 261 | it('should render with proper CSS class structure', async () => { 262 | const vimeoId = '123456789'; 263 | 264 | render(Vimeo, { 265 | vimeoId, 266 | disable_observer: true, 267 | }); 268 | const wrapper = page.getByTestId('vimeo'); 269 | 270 | await expect 271 | .element(wrapper) 272 | .toHaveAttribute('class', 'vimeo-svelte-embed'); 273 | }); 274 | 275 | it.skip('should handle skipTo with partial time values', async () => { 276 | const testCases = [ 277 | { skipTo: { h: 1, m: 0, s: 0 }, expected: '1h0m0s' }, 278 | { skipTo: { h: 0, m: 30, s: 0 }, expected: '0h30m0s' }, 279 | { skipTo: { h: 0, m: 0, s: 45 }, expected: '0h0m45s' }, 280 | { skipTo: { h: 2, m: 15, s: 0 }, expected: '2h15m0s' }, 281 | ]; 282 | 283 | for (const testCase of testCases) { 284 | const vimeoId = '123456789'; 285 | render(Vimeo, { 286 | vimeoId, 287 | skipTo: testCase.skipTo, 288 | disable_observer: true, 289 | }); 290 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 291 | 292 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=${testCase.expected}`; 293 | await expect 294 | .element(iframe) 295 | .toHaveAttribute('src', expected_src); 296 | } 297 | }); 298 | 299 | it('should handle numeric vimeoId values', async () => { 300 | const vimeoId = '987654321'; 301 | 302 | render(Vimeo, { 303 | vimeoId, 304 | disable_observer: true, 305 | }); 306 | const iframe = page.getByTitle(`vimeo-${vimeoId}`); 307 | 308 | const expected_src = `https://player.vimeo.com/video/${vimeoId}?autoplay=false#t=0h0m0s`; 309 | await expect.element(iframe).toHaveAttribute('src', expected_src); 310 | }); 311 | 312 | it('should handle undefined aspect ratio gracefully', async () => { 313 | const vimeoId = '123456789'; 314 | 315 | // Test with invalid aspect ratio - should fallback to default 316 | const { container } = render(Vimeo, { 317 | vimeoId, 318 | aspectRatio: 'invalid:ratio' as any, 319 | disable_observer: true, 320 | }); 321 | const wrapper = container.querySelector('.vimeo-svelte-embed'); 322 | 323 | // Should not have padding-top since aspect ratio is invalid 324 | expect(wrapper?.getAttribute('style')).not.toContain( 325 | 'padding-top:', 326 | ); 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/you-tube.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | 57 |
64 | 80 |
81 |
82 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/components/zencastr.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |
25 | 29 | 35 | View on Zencastr 36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AnchorFm } from './components/anchor-fm.svelte'; 2 | export { default as Bluesky } from './components/bluesky.svelte'; 3 | export { default as Buzzsprout } from './components/buzzsprout.svelte'; 4 | export { default as CodePen } from './components/code-pen.svelte'; 5 | export { default as Deezer } from './components/deezer.svelte'; 6 | export { default as GeneralObserver } from './components/general-observer.svelte'; 7 | export { default as GenericEmbed } from './components/generic-embed.svelte'; 8 | export { default as Gist } from './components/gist.svelte'; 9 | export { default as Guild } from './components/guild.svelte'; 10 | export { default as Relive } from './components/relive.svelte'; 11 | export { default as SimpleCast } from './components/simple-cast.svelte'; 12 | export { default as Slides } from './components/slides.svelte'; 13 | export { default as SoundCloud } from './components/sound-cloud.svelte'; 14 | export { default as Spotify } from './components/spotify.svelte'; 15 | export { default as StackBlitz } from './components/stackblitz.svelte'; 16 | export { default as TikTok } from './components/tiktok.svelte'; 17 | export { default as Toot } from './components/toot.svelte'; 18 | export { default as Tweet } from './components/tweet.svelte'; 19 | export { default as Vimeo } from './components/vimeo.svelte'; 20 | export { default as YouTube } from './components/you-tube.svelte'; 21 | export { default as Zencastr } from './components/zencastr.svelte'; 22 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | [key: string]: string; 3 | }; 4 | 5 | const config: Config = { 6 | '1:1': `padding-top: 100%;`, 7 | '16:9': `padding-top: 56.25%;`, 8 | '4:3': `padding-top: 75%;`, 9 | '3:2': `padding-top: 66.66%;`, 10 | '8.5': `padding-top: 62.5%;`, 11 | }; 12 | 13 | export const getPadding = (aspectRatio: keyof Config) => { 14 | return config[aspectRatio]; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to your library project

2 |

3 | Create your package using @sveltejs/package and preview/showcase 4 | your work with SvelteKit 5 |

6 |

7 | Visit kit.svelte.dev to read the 8 | documentation 9 |

10 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-embed/42dd1f54bea3305cd0ffb5792a2a00072027029e/packages/sveltekit-embed/static/favicon.png -------------------------------------------------------------------------------- /packages/sveltekit-embed/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter(), 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | import { coverageConfigDefaults } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | test: { 8 | workspace: [ 9 | { 10 | extends: './vite.config.ts', 11 | test: { 12 | name: 'client', 13 | environment: 'browser', 14 | browser: { 15 | enabled: true, 16 | provider: 'playwright', 17 | instances: [ 18 | { 19 | browser: 'chromium', 20 | }, 21 | ], 22 | }, 23 | clearMocks: true, 24 | include: ['src/**/*.svelte.{test,spec}.{js,ts}'], 25 | exclude: ['src/lib/server/**'], 26 | setupFiles: ['./vitest-setup-client.ts'], 27 | }, 28 | }, 29 | { 30 | extends: './vite.config.ts', 31 | test: { 32 | name: 'server', 33 | environment: 'node', 34 | include: ['src/**/*.{test,spec}.{js,ts}'], 35 | exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'], 36 | }, 37 | }, 38 | ], 39 | coverage: { 40 | all: true, 41 | reporter: ['text-summary', 'html'], 42 | exclude: [ 43 | ...coverageConfigDefaults.exclude, 44 | '**/config.{js,ts,cjs}', 45 | '**/*.config.{js,ts,cjs}', 46 | '**/e2e/**', 47 | '**/lib/index.ts', 48 | '**/routes/page.svelte', 49 | ], 50 | thresholds: { 51 | statements: 80, 52 | branches: 80, 53 | functions: 80, 54 | lines: 80, 55 | }, 56 | }, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /packages/sveltekit-embed/vitest-setup-client.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | --------------------------------------------------------------------------------