├── .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 | [](#contributors-)
6 |
7 |
8 |
9 | [](https://madewithsvelte.com/p/sveltekit-embed/shield-link)
10 |
11 | [](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 | 
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 |
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 |
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 |
13 |
--------------------------------------------------------------------------------
/apps/web/src/lib/icons/you-tube.svelte:
--------------------------------------------------------------------------------
1 |
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 |
51 |
52 | {#if !data.is_cloudflare}
53 |
57 |
60 | We've moved to Cloudflare!
61 |
62 |
63 | This is now being hosted on Cloudflare Pages. You can check it
64 | out on
68 | https://sveltekit-embed.pages.dev
69 |
70 |
71 |
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 | [](https://badge.fury.io/js/sveltekit-embed)
4 | [](https://www.npmjs.com/package/sveltekit-embed)
5 | [](https://opensource.org/licenses/MIT)
6 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------