diff --git a/.eslintrc.js b/.eslintrc.js index db665ad..4313e0e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,4 +13,4 @@ module.exports = { 'no-console': 'warn', 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, -}; \ No newline at end of file +}; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4085987..543adcb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: test: @@ -13,99 +13,99 @@ jobs: matrix: node-version: [20.x] steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libgbm-dev jq xvfb - npm ci + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgbm-dev jq xvfb + npm ci - - name: Lint - run: | - npm run lint - npm run format -- --check + - name: Lint + run: | + npm run lint + npm run format -- --check - - name: Install Chrome - uses: browser-actions/setup-chrome@latest - with: - chrome-version: stable + - name: Install Chrome + uses: browser-actions/setup-chrome@latest + with: + chrome-version: stable - - name: Run tests with coverage - run: | - # Run tests with coverage and capture results in JSON format - npm test -- --coverage-reporters=lcov --coverage-reporters=json-summary --json --outputFile=test-results.json + - name: Run tests with coverage + run: | + # Run tests with coverage and capture results in JSON format + npm test -- --coverage-reporters=lcov --coverage-reporters=json-summary --json --outputFile=test-results.json - # Extract coverage from JSON and format for summary - echo "# Test Results (Node ${{ matrix.node-version }})" >> $GITHUB_STEP_SUMMARY - echo "## Coverage" >> $GITHUB_STEP_SUMMARY - echo "| Type | Coverage | Details |" >> $GITHUB_STEP_SUMMARY - echo "|------|----------|----------|" >> $GITHUB_STEP_SUMMARY + # Extract coverage from JSON and format for summary + echo "# Test Results (Node ${{ matrix.node-version }})" >> $GITHUB_STEP_SUMMARY + echo "## Coverage" >> $GITHUB_STEP_SUMMARY + echo "| Type | Coverage | Details |" >> $GITHUB_STEP_SUMMARY + echo "|------|----------|----------|" >> $GITHUB_STEP_SUMMARY - # Use jq to parse the coverage JSON - jq -r '.total | - "| Statements | \(.statements.pct)% | \(.statements.covered)/\(.statements.total) |\n| Branches | \(.branches.pct)% | \(.branches.covered)/\(.branches.total) |\n| Functions | \(.functions.pct)% | \(.functions.covered)/\(.functions.total) |\n| Lines | \(.lines.pct)% | \(.lines.covered)/\(.lines.total) |"' coverage/coverage-summary.json >> $GITHUB_STEP_SUMMARY || { - echo "| Coverage data not available | - | - |" >> $GITHUB_STEP_SUMMARY - } + # Use jq to parse the coverage JSON + jq -r '.total | + "| Statements | \(.statements.pct)% | \(.statements.covered)/\(.statements.total) |\n| Branches | \(.branches.pct)% | \(.branches.covered)/\(.branches.total) |\n| Functions | \(.functions.pct)% | \(.functions.covered)/\(.functions.total) |\n| Lines | \(.lines.pct)% | \(.lines.covered)/\(.lines.total) |"' coverage/coverage-summary.json >> $GITHUB_STEP_SUMMARY || { + echo "| Coverage data not available | - | - |" >> $GITHUB_STEP_SUMMARY + } - # Add test results using jq - echo "## Test Results" >> $GITHUB_STEP_SUMMARY - echo "| Total | Passed | Failed | Skipped |" >> $GITHUB_STEP_SUMMARY - echo "|-------|---------|---------|----------|" >> $GITHUB_STEP_SUMMARY - jq -r '"| \(.numTotalTests) | \(.numPassedTests) | \(.numFailedTests) | \(.numPendingTests) |"' test-results.json >> $GITHUB_STEP_SUMMARY + # Add test results using jq + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + echo "| Total | Passed | Failed | Skipped |" >> $GITHUB_STEP_SUMMARY + echo "|-------|---------|---------|----------|" >> $GITHUB_STEP_SUMMARY + jq -r '"| \(.numTotalTests) | \(.numPassedTests) | \(.numFailedTests) | \(.numPendingTests) |"' test-results.json >> $GITHUB_STEP_SUMMARY - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: coverage/ + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ - - name: Run visual tests - run: | - mkdir -p __image_snapshots__ - CHROME_PATH="$(which chrome)" xvfb-run --auto-servernum --server-args="-screen 0 1280x800x24" npm run test:visual:ci -- -u --json --outputFile=visual-test-results.json + - name: Run visual tests + run: | + mkdir -p __image_snapshots__ + CHROME_PATH="$(which chrome)" xvfb-run --auto-servernum --server-args="-screen 0 1280x800x24" npm run test:visual:ci -- -u --json --outputFile=visual-test-results.json - # Show test results - if [ -f "visual-test-results.json" ]; then - echo "## Visual Test Results" >> $GITHUB_STEP_SUMMARY - echo "| Total | Passed | Failed | Skipped |" >> $GITHUB_STEP_SUMMARY - echo "|-------|---------|---------|----------|" >> $GITHUB_STEP_SUMMARY - jq -r '"| \(.numTotalTests) | \(.numPassedTests) | \(.numFailedTests) | \(.numPendingTests) |"' visual-test-results.json >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - fi + # Show test results + if [ -f "visual-test-results.json" ]; then + echo "## Visual Test Results" >> $GITHUB_STEP_SUMMARY + echo "| Total | Passed | Failed | Skipped |" >> $GITHUB_STEP_SUMMARY + echo "|-------|---------|---------|----------|" >> $GITHUB_STEP_SUMMARY + jq -r '"| \(.numTotalTests) | \(.numPassedTests) | \(.numFailedTests) | \(.numPendingTests) |"' visual-test-results.json >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi - # Show screenshots in summary - { - echo "## Screenshots" - echo "" - for img in __image_snapshots__/ci-*.png; do - if [ -f "$img" ]; then - name=$(basename "$img" | sed -E 's/ci-Zapier Visual Tests (.+)-1\.png/\1/') - echo "### $name" - echo "" - echo "
" - echo "View Screenshot" - echo "" - encoded=$(cat "$img" | base64) - echo "" - echo "" - echo "
" - echo "" - fi - done - } >> $GITHUB_STEP_SUMMARY + # Show screenshots in summary + { + echo "## Screenshots" + echo "" + for img in __image_snapshots__/ci-*.png; do + if [ -f "$img" ]; then + name=$(basename "$img" | sed -E 's/ci-Zapier Visual Tests (.+)-1\.png/\1/') + echo "### $name" + echo "" + echo "
" + echo "View Screenshot" + echo "" + encoded=$(cat "$img" | base64) + echo "" + echo "" + echo "
" + echo "" + fi + done + } >> $GITHUB_STEP_SUMMARY - # Create a flat directory for artifacts - mkdir -p artifacts - cp __image_snapshots__/*.png artifacts/ + # Create a flat directory for artifacts + mkdir -p artifacts + cp __image_snapshots__/*.png artifacts/ - - name: Upload artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: screenshots - path: artifacts/ \ No newline at end of file + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: artifacts/ diff --git a/.github/workflows/zapier-release.yml b/.github/workflows/zapier-release.yml index 807a5db..5f4d54a 100644 --- a/.github/workflows/zapier-release.yml +++ b/.github/workflows/zapier-release.yml @@ -3,7 +3,7 @@ name: Zapier Release on: push: tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: deploy: @@ -29,4 +29,4 @@ jobs: - name: Deploy to Zapier env: ZAPIER_DEPLOY_KEY: ${{ secrets.ZAPIER_DEPLOY_KEY }} - run: zapier push --skip-npm-install \ No newline at end of file + run: zapier push --skip-npm-install diff --git a/.markdownlint.json b/.markdownlint.json index 73cb980..67d2ae5 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,3 +1,3 @@ { "MD013": false -} \ No newline at end of file +} diff --git a/.prettierrc b/.prettierrc index 860c5a7..a95cb05 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,4 +4,4 @@ "trailingComma": "es5", "printWidth": 100, "tabWidth": 2 -} \ No newline at end of file +} diff --git a/README.md b/README.md index fb7cc6c..11ba21e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Screenly Zapier Integration -[![Test]( - https://github.com/screenly/zapier/actions/workflows/test.yml/badge.svg?branch=master -)]( - https://github.com/screenly/zapier/actions/workflows/test.yml -) +[![Test](https://github.com/screenly/zapier/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/screenly/zapier/actions/workflows/test.yml) A Zapier integration for automating Screenly digital signage operations. Connect your Screenly displays with 5000+ apps. @@ -13,28 +9,26 @@ Screenly displays with 5000+ apps. 1. **Install the Integration** - * Go to [Zapier's Screenly Integration]( - https://zapier.com/apps/screenly/integrations - ) page - * Click "Connect Screenly" - * When prompted, enter your Screenly API key from + - Go to [Zapier's Screenly Integration](https://zapier.com/apps/screenly/integrations) page + - Click "Connect Screenly" + - When prompted, enter your Screenly API key from [Screenly Settings](https://app.screenlyapp.com/settings/api-keys) 2. **Create Your First Zap** - * Click "Make a Zap" - * Choose your trigger app (e.g., Dropbox, SharePoint, Google Drive) - * Choose Screenly as your action app - * Select one of these actions: - * Upload Asset - Add new content to your Screenly library - * Add to Playlist - Add content to a specific playlist - * Complete Workflow - Upload, add to playlist, and assign to screens + - Click "Make a Zap" + - Choose your trigger app (e.g., Dropbox, SharePoint, Google Drive) + - Choose Screenly as your action app + - Select one of these actions: + - Upload Asset - Add new content to your Screenly library + - Add to Playlist - Add content to a specific playlist + - Complete Workflow - Upload, add to playlist, and assign to screens 3. **Example: Auto-Upload from Dropbox** - * Trigger: New file in Dropbox folder - * Action: Screenly - Upload Asset - * Test your Zap and turn it on + - Trigger: New file in Dropbox folder + - Action: Screenly - Upload Asset + - Test your Zap and turn it on ## Available Actions @@ -42,112 +36,116 @@ Screenly displays with 5000+ apps. Upload files to your Screenly account: -* Supports images, videos, and URLs -* Set custom titles and durations -* Accepts files via URL or direct upload +- Supports images, videos, and URLs +- Set custom titles and durations +- Accepts files via URL or direct upload ### 2. Add to Playlist Add content to your playlists: -* Choose from your existing playlists -* Set display duration -* Schedule content with start/end dates -* Select target screens +- Choose from your existing playlists +- Set display duration +- Schedule content with start/end dates +- Select target screens ### 3. Complete Workflow Do everything in one step: -* Upload new content -* Add to playlist -* Assign to screens -* Set schedule -* Perfect for automated content management +- Upload new content +- Add to playlist +- Assign to screens +- Set schedule +- Perfect for automated content management ### 4. Cleanup Maintain your content library: -* Remove Zapier-created content -* Automatic cleanup based on rules -* Safe deletion with confirmation +- Remove Zapier-created content +- Automatic cleanup based on rules +- Safe deletion with confirmation ### 5. Enable/Disable Playlist Control playlist visibility dynamically: -* Enable or disable playlists based on conditions -* Schedule playlist activation periods -* Perfect for: - * Weather-based content - * Time-sensitive promotions - * Event-driven displays - * Seasonal campaigns +- Enable or disable playlists based on conditions +- Schedule playlist activation periods +- Perfect for: + - Weather-based content + - Time-sensitive promotions + - Event-driven displays + - Seasonal campaigns ## Common Use Cases 1. **Automated Content Updates** - * Connect Dropbox/Google Drive/SharePoint - * New files automatically appear on screens - * Perfect for marketing teams + + - Connect Dropbox/Google Drive/SharePoint + - New files automatically appear on screens + - Perfect for marketing teams 2. **Scheduled Displays** - * Show content during specific times - * Automate weekly/monthly updates - * Event-based content management + + - Show content during specific times + - Automate weekly/monthly updates + - Event-based content management 3. **Multi-Screen Management** - * Different content for different locations - * Department-specific displays - * Emergency broadcast system + + - Different content for different locations + - Department-specific displays + - Emergency broadcast system 4. **Content Library Management** - * Automatic file organization - * Scheduled content rotation - * Cleanup old content + + - Automatic file organization + - Scheduled content rotation + - Cleanup old content 5. **Weather-Based Dynamic Content** - * **Example: Smart Weather-Driven Displays** - * Use Weather API as trigger (hourly check) - * **Temperature-Based Content:** - * When temperature > 20°C: - * Enable "Summer Treats" playlist (ice cream, cold drinks) - * Disable "Winter Warmers" playlist - * When temperature < 0°C: - * Enable "Winter Warmers" playlist (hot chocolate, soups) - * Disable "Summer Treats" playlist - * **Rain-Based Content:** - * When it's raining: - * Enable "Rainy Day Specials" playlist (umbrellas, raincoats) - * Show "Stay Dry" promotions - * When rain stops: - * Return to default seasonal playlist - * Disable rain-specific promotions - * **Setup Steps:** + - **Example: Smart Weather-Driven Displays** + - Use Weather API as trigger (hourly check) + - **Temperature-Based Content:** + - When temperature > 20°C: + - Enable "Summer Treats" playlist (ice cream, cold drinks) + - Disable "Winter Warmers" playlist + - When temperature < 0°C: + - Enable "Winter Warmers" playlist (hot chocolate, soups) + - Disable "Summer Treats" playlist + - **Rain-Based Content:** + - When it's raining: + - Enable "Rainy Day Specials" playlist (umbrellas, raincoats) + - Show "Stay Dry" promotions + - When rain stops: + - Return to default seasonal playlist + - Disable rain-specific promotions + - **Setup Steps:** 1. Create weather-specific playlists in Screenly: - * "Summer Treats" - cold items - * "Winter Warmers" - hot items - * "Rainy Day Specials" - weather protection items + - "Summer Treats" - cold items + - "Winter Warmers" - hot items + - "Rainy Day Specials" - weather protection items 2. In Zapier: - * Trigger: Weather by Zapier (check every hour) - * Filter 1: Temperature conditions - * Filter 2: Precipitation conditions - * Actions: - * Enable/Disable appropriate playlists - * Update content based on current conditions - * **Benefits:** - * Automatically adapt to multiple weather conditions - * Increase sales with contextual promotions - * Show relevant products at the right time - * Create urgency with weather-specific offers - * No manual playlist management needed + - Trigger: Weather by Zapier (check every hour) + - Filter 1: Temperature conditions + - Filter 2: Precipitation conditions + - Actions: + - Enable/Disable appropriate playlists + - Update content based on current conditions + - **Benefits:** + - Automatically adapt to multiple weather conditions + - Increase sales with contextual promotions + - Show relevant products at the right time + - Create urgency with weather-specific offers + - No manual playlist management needed ## Security -* API keys are stored securely by Zapier -* All API requests are made over HTTPS -* The integration follows Zapier's security best practices +- API keys are stored securely by Zapier +- All API requests are made over HTTPS +- The integration follows Zapier's security best practices ## Example Integrations @@ -156,60 +154,66 @@ Control playlist visibility dynamically: #### Dropbox 1. **Simple Asset Upload** - * Trigger: New file in Dropbox folder - * Action: Upload to Screenly - * Use Case: Quickly add new content to your Screenly library + + - Trigger: New file in Dropbox folder + - Action: Upload to Screenly + - Use Case: Quickly add new content to your Screenly library 2. **Automated Playlist Management** - * Trigger: New file in specific Dropbox folder - * Action: Complete Workflow (Upload + Playlist + Screen) - * Use Case: Different folders map to different screens/playlists + - Trigger: New file in specific Dropbox folder + - Action: Complete Workflow (Upload + Playlist + Screen) + - Use Case: Different folders map to different screens/playlists #### Box 1. **Content Library Management** - * Trigger: New file in Box folder - * Action: Upload to Screenly with metadata - * Use Case: Use Box metadata for asset scheduling + + - Trigger: New file in Box folder + - Action: Upload to Screenly with metadata + - Use Case: Use Box metadata for asset scheduling 2. **Multi-Screen Campaign** - * Trigger: New file with specific tag in Box - * Action: Complete Workflow - * Use Case: Marketing campaigns across multiple screens + - Trigger: New file with specific tag in Box + - Action: Complete Workflow + - Use Case: Marketing campaigns across multiple screens #### SharePoint 1. **Corporate Communications** - * Trigger: New file in SharePoint document library - * Action: Complete Workflow - * Use Case: Display company announcements on office screens + + - Trigger: New file in SharePoint document library + - Action: Complete Workflow + - Use Case: Display company announcements on office screens 2. **Department-Specific Content** - * Trigger: New file in department SharePoint folder - * Action: Upload + Add to Department Playlist - * Use Case: Each department manages their own digital signage content + + - Trigger: New file in department SharePoint folder + - Action: Upload + Add to Department Playlist + - Use Case: Each department manages their own digital signage content 3. **Event Display Management** - * Trigger: New item in SharePoint Events list - * Action: Complete Workflow with scheduling - * Use Case: Automatically display event information during relevant times + + - Trigger: New item in SharePoint Events list + - Action: Complete Workflow with scheduling + - Use Case: Automatically display event information during relevant times 4. **Multi-Location Content Distribution** - * Trigger: New file in SharePoint with location metadata - * Action: Complete Workflow with screen selection - * Use Case: Distribute content to specific office locations based on SharePoint metadata + - Trigger: New file in SharePoint with location metadata + - Action: Complete Workflow with screen selection + - Use Case: Distribute content to specific office locations based on SharePoint metadata ### Automated Maintenance 1. **Content Cleanup** - * Trigger: Schedule (e.g., weekly) - * Action: Cleanup Zapier Content - * Use Case: Maintain a clean asset library by removing old content + + - Trigger: Schedule (e.g., weekly) + - Action: Cleanup Zapier Content + - Use Case: Maintain a clean asset library by removing old content 2. **Content Rotation** - * Trigger: Schedule or SharePoint/Dropbox/Box update - * Action: Complete Workflow with end dates - * Use Case: Automatically rotate content based on schedules + - Trigger: Schedule or SharePoint/Dropbox/Box update + - Action: Complete Workflow with end dates + - Use Case: Automatically rotate content based on schedules --- @@ -217,9 +221,9 @@ Control playlist visibility dynamically: ### Prerequisites -* Node.js 20.x -* npm -* A Screenly account with API access +- Node.js 20.x +- npm +- A Screenly account with API access ### Installation @@ -246,33 +250,33 @@ Control playlist visibility dynamically: ``` 3. Configure your Screenly API key: - * Get your API key from [Screenly Settings](https://app.screenlyapp.com/settings/api-keys) - * When setting up the Zapier integration, you'll be prompted to enter this API key + - Get your API key from [Screenly Settings](https://app.screenlyapp.com/settings/api-keys) + - When setting up the Zapier integration, you'll be prompted to enter this API key ### Available Scripts -* `npm test` - Run unit tests with coverage -* `npm run test:watch` - Run tests in watch mode -* `npm run lint` - Run ESLint -* `npm run format` - Format code with Prettier -* `npm run clean` - Clean up generated files -* `npm run prepare` - Install git hooks (runs automatically after npm install) +- `npm test` - Run unit tests with coverage +- `npm run test:watch` - Run tests in watch mode +- `npm run lint` - Run ESLint +- `npm run format` - Format code with Prettier +- `npm run clean` - Clean up generated files +- `npm run prepare` - Install git hooks (runs automatically after npm install) Visual tests are only run in CI environment: -* `npm run test:visual` - Displays information about visual tests -* `npm run test:visual:ci` - Runs visual tests (CI only) +- `npm run test:visual` - Displays information about visual tests +- `npm run test:visual:ci` - Runs visual tests (CI only) ### Code Quality The project uses several tools to ensure code quality: -* **ESLint** - For code linting -* **Prettier** - For code formatting -* **Jest** - For testing -* **Husky** - For git hooks -* **lint-staged** - For running checks on staged files -* **markdownlint** - For markdown formatting +- **ESLint** - For code linting +- **Prettier** - For code formatting +- **Jest** - For testing +- **Husky** - For git hooks +- **lint-staged** - For running checks on staged files +- **markdownlint** - For markdown formatting These run automatically on commit, but you can also run them manually: @@ -285,27 +289,27 @@ npm run format # Fix code formatting Pre-commit hooks are set up to: -* Run unit tests -* Lint JavaScript files -* Format code with Prettier -* Check markdown formatting -* Run tests +- Run unit tests +- Lint JavaScript files +- Format code with Prettier +- Check markdown formatting +- Run tests The hooks are configured in: -* `.husky/pre-commit` - Hook scripts -* `package.json` - lint-staged configuration -* `.eslintrc.js` - ESLint rules -* `.prettierrc` - Prettier configuration -* `.markdownlint.json` - Markdown linting rules +- `.husky/pre-commit` - Hook scripts +- `package.json` - lint-staged configuration +- `.eslintrc.js` - ESLint rules +- `.prettierrc` - Prettier configuration +- `.markdownlint.json` - Markdown linting rules ### Visual Testing Visual tests are automatically run in CI and generate screenshots of: -* Upload Asset Form -* Complete Workflow Form -* Cleanup Confirmation Form +- Upload Asset Form +- Complete Workflow Form +- Cleanup Confirmation Form These tests are skipped locally to avoid environment-specific issues. @@ -313,8 +317,8 @@ These tests are skipped locally to avoid environment-specific issues. 1. **Dependency Management** - * Never edit package-lock.json manually - * Use npm commands to manage dependencies: + - Never edit package-lock.json manually + - Use npm commands to manage dependencies: ```bash npm install # Add dependency @@ -324,37 +328,41 @@ These tests are skipped locally to avoid environment-specific issues. ``` 2. **Code Style** - * ESLint and Prettier are configured - * Formatting is automatically handled on commit - * Run `npm run format` to manually format code + + - ESLint and Prettier are configured + - Formatting is automatically handled on commit + - Run `npm run format` to manually format code 3. **Testing** - * Maintain test coverage above 80% - * Write tests for new features - * Visual tests are CI-only + + - Maintain test coverage above 80% + - Write tests for new features + - Visual tests are CI-only 4. **Git Workflow** - * Commits are automatically linted - * Visual tests run on pull requests - * CI checks must pass before merge + - Commits are automatically linted + - Visual tests run on pull requests + - CI checks must pass before merge ### Deployment The integration is automatically deployed to Zapier when a new version tag is pushed to GitHub. 1. Create and push a new version tag: + ```bash git tag -a v0.1.0 -m "Initial release" git push origin v0.1.0 ``` 2. The GitHub Action will: - * Run tests - * Deploy to Zapier + - Run tests + - Deploy to Zapier ### Setting up Zapier Deployment 1. Get your Zapier Deploy Key: + ```bash zapier login zapier register "Screenly" # Only needed for first-time setup @@ -362,15 +370,15 @@ The integration is automatically deployed to Zapier when a new version tag is pu ``` 2. Add the deploy key to GitHub: - * Go to your repository's Settings > Secrets > Actions - * Add a new secret named `ZAPIER_DEPLOY_KEY` - * Paste your deploy key as the value + - Go to your repository's Settings > Secrets > Actions + - Add a new secret named `ZAPIER_DEPLOY_KEY` + - Paste your deploy key as the value ### Version Management -* Use semantic versioning (MAJOR.MINOR.PATCH) -* Tag format: `v*.*.*` (e.g., v0.1.0, v1.0.0) -* Pre-release versions: Use `-beta`, `-alpha` suffixes +- Use semantic versioning (MAJOR.MINOR.PATCH) +- Tag format: `v*.*.*` (e.g., v0.1.0, v1.0.0) +- Pre-release versions: Use `-beta`, `-alpha` suffixes ### License diff --git a/index.js b/index.js index e70eb0a..25f7574 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,10 @@ const authentication = { type: 'custom', test: { headers: { - Authorization: 'Token {{bundle.authData.api_key}}' + Authorization: 'Token {{bundle.authData.api_key}}', }, url: 'https://api.screenlyapp.com/api/v4/assets/', - method: 'GET' + method: 'GET', }, fields: [ { @@ -17,10 +17,10 @@ const authentication = { type: 'string', required: true, label: 'API Key', - helpText: 'Your Screenly API key from https://app.screenlyapp.com/settings/api-keys' - } + helpText: 'Your Screenly API key from https://app.screenlyapp.com/settings/api-keys', + }, ], - connectionLabel: '{{bundle.authData.api_key}}' + connectionLabel: '{{bundle.authData.api_key}}', }; // Upload Asset Action @@ -29,7 +29,7 @@ const uploadAsset = { noun: 'Asset', display: { label: 'Upload Asset', - description: 'Upload a new asset to Screenly' + description: 'Upload a new asset to Screenly', }, operation: { inputFields: [ @@ -38,14 +38,14 @@ const uploadAsset = { label: 'File URL', type: 'string', required: true, - helpText: 'The URL of the file to upload' + helpText: 'The URL of the file to upload', }, { key: 'title', label: 'Title', type: 'string', required: true, - helpText: 'Title of the asset' + helpText: 'Title of the asset', }, { key: 'duration', @@ -53,14 +53,14 @@ const uploadAsset = { type: 'integer', required: false, default: '10', - helpText: 'How long should the asset be shown (in seconds)' - } + helpText: 'How long should the asset be shown (in seconds)', + }, ], perform: async (z, bundle) => { // First, fetch the file const fileResponse = await z.request({ url: bundle.inputData.file, - raw: true + raw: true, }); if (fileResponse.status >= 400) { @@ -77,9 +77,9 @@ const uploadAsset = { url: 'https://api.screenlyapp.com/api/v4/assets/', method: 'POST', headers: { - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, - body: formData + body: formData, }); return utils.handleError(response, 'Failed to upload asset'); @@ -89,9 +89,9 @@ const uploadAsset = { title: 'Sample Asset', duration: 10, type: 'image', - url: 'https://example.com/sample.jpg' - } - } + url: 'https://example.com/sample.jpg', + }, + }, }; // Schedule Playlist Item Action @@ -100,7 +100,7 @@ const schedulePlaylistItem = { noun: 'Playlist Item', display: { label: 'Add Asset to Playlist', - description: 'Add an asset to a playlist with scheduling' + description: 'Add an asset to a playlist with scheduling', }, operation: { inputFields: [ @@ -110,7 +110,7 @@ const schedulePlaylistItem = { type: 'string', required: true, dynamic: 'get_playlists.id.name', - helpText: 'Select the playlist' + helpText: 'Select the playlist', }, { key: 'asset_id', @@ -118,7 +118,7 @@ const schedulePlaylistItem = { type: 'string', required: true, dynamic: 'get_assets.id.title', - helpText: 'Select the asset to schedule' + helpText: 'Select the asset to schedule', }, { key: 'duration', @@ -126,22 +126,22 @@ const schedulePlaylistItem = { type: 'integer', required: false, default: '10', - helpText: 'How long should this asset be shown (in seconds)' + helpText: 'How long should this asset be shown (in seconds)', }, { key: 'start_date', label: 'Start Date', type: 'datetime', required: false, - helpText: 'When should this item start being available for playback (optional)' + helpText: 'When should this item start being available for playback (optional)', }, { key: 'end_date', label: 'End Date', type: 'datetime', required: false, - helpText: 'When should this item stop being available (optional)' - } + helpText: 'When should this item stop being available (optional)', + }, ], perform: async (z, bundle) => { // First, update the asset duration if specified @@ -151,11 +151,11 @@ const schedulePlaylistItem = { method: 'PATCH', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, body: { - duration: parseInt(bundle.inputData.duration, 10) - } + duration: parseInt(bundle.inputData.duration, 10), + }, }); if (assetResponse.status >= 400) { @@ -174,7 +174,7 @@ const schedulePlaylistItem = { const payload = { asset: bundle.inputData.asset_id, - playlist: bundle.inputData.playlist_id + playlist: bundle.inputData.playlist_id, }; // Only add conditions if they are specified @@ -187,9 +187,9 @@ const schedulePlaylistItem = { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, - body: payload + body: payload, }); if (response.status >= 400) { @@ -204,10 +204,10 @@ const schedulePlaylistItem = { playlist: 1, conditions: { start_date: '2024-01-01T00:00:00Z', - end_date: '2024-12-31T23:59:59Z' - } - } - } + end_date: '2024-12-31T23:59:59Z', + }, + }, + }, }; // Assign Screen to Playlist Action @@ -216,7 +216,7 @@ const assignScreenToPlaylist = { noun: 'Screen Assignment', display: { label: 'Assign Screen to Playlist', - description: 'Assign a screen to play a specific playlist' + description: 'Assign a screen to play a specific playlist', }, operation: { inputFields: [ @@ -226,7 +226,7 @@ const assignScreenToPlaylist = { type: 'string', required: true, dynamic: 'get_screens.id.name', - helpText: 'Select the screen to assign' + helpText: 'Select the screen to assign', }, { key: 'playlist_id', @@ -234,8 +234,8 @@ const assignScreenToPlaylist = { type: 'string', required: true, dynamic: 'get_playlists.id.name', - helpText: 'Select the playlist to assign to the screen' - } + helpText: 'Select the playlist to assign to the screen', + }, ], perform: async (z, bundle) => { const response = await z.request({ @@ -243,11 +243,11 @@ const assignScreenToPlaylist = { method: 'PATCH', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, body: { - playlist: bundle.inputData.playlist_id - } + playlist: bundle.inputData.playlist_id, + }, }); if (response.status >= 400) { @@ -259,9 +259,9 @@ const assignScreenToPlaylist = { sample: { id: 1, name: 'Sample Screen', - playlist: 1 - } - } + playlist: 1, + }, + }, }; // Get Screens Trigger @@ -270,15 +270,15 @@ const getScreens = { noun: 'Screen', display: { label: 'Get Screens', - description: 'Triggers when listing available Screenly screens.' + description: 'Triggers when listing available Screenly screens.', }, operation: { perform: async (z, bundle) => { const response = await z.request({ url: 'https://api.screenlyapp.com/api/v4/screens/', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); return utils.handleError(response, 'Failed to fetch screens'); @@ -287,9 +287,9 @@ const getScreens = { id: 1, name: 'Sample Screen', description: 'A sample screen', - playlist: 1 - } - } + playlist: 1, + }, + }, }; // Get Playlists Trigger @@ -298,15 +298,15 @@ const getPlaylists = { noun: 'Playlist', display: { label: 'Get Playlists', - description: 'Triggers when listing available Screenly playlists.' + description: 'Triggers when listing available Screenly playlists.', }, operation: { perform: async (z, bundle) => { const response = await z.request({ url: 'https://api.screenlyapp.com/api/v4/playlists/', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); return utils.handleError(response, 'Failed to fetch playlists'); @@ -315,9 +315,9 @@ const getPlaylists = { id: 1, name: 'Sample Playlist', description: 'A sample playlist', - items_count: 5 - } - } + items_count: 5, + }, + }, }; // Get Assets Trigger @@ -326,15 +326,15 @@ const getAssets = { noun: 'Asset', display: { label: 'Get Assets', - description: 'Triggers when listing available Screenly assets.' + description: 'Triggers when listing available Screenly assets.', }, operation: { perform: async (z, bundle) => { const response = await z.request({ url: 'https://api.screenlyapp.com/api/v4/assets/', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); return utils.handleError(response, 'Failed to fetch assets'); @@ -344,9 +344,9 @@ const getAssets = { title: 'Sample Asset', type: 'image', duration: 10, - url: 'https://example.com/sample.jpg' - } - } + url: 'https://example.com/sample.jpg', + }, + }, }; // Helper to tag resources created by Zapier @@ -358,7 +358,7 @@ const completeWorkflow = { noun: 'Workflow', display: { label: 'Complete Workflow', - description: 'Upload asset, create/select playlist, and assign to screen' + description: 'Upload asset, create/select playlist, and assign to screen', }, operation: { inputFields: [ @@ -367,14 +367,14 @@ const completeWorkflow = { label: 'File URL', type: 'string', required: true, - helpText: 'The URL of the file to upload' + helpText: 'The URL of the file to upload', }, { key: 'title', label: 'Asset Title', type: 'string', required: true, - helpText: 'Title of the asset' + helpText: 'Title of the asset', }, { key: 'duration', @@ -382,7 +382,7 @@ const completeWorkflow = { type: 'integer', required: false, default: '10', - helpText: 'How long should the asset be shown (in seconds)' + helpText: 'How long should the asset be shown (in seconds)', }, { key: 'playlist_id', @@ -390,14 +390,14 @@ const completeWorkflow = { type: 'string', required: false, dynamic: 'get_playlists.id.name', - helpText: 'Select an existing playlist or create a new one' + helpText: 'Select an existing playlist or create a new one', }, { key: 'new_playlist_name', label: 'New Playlist Name', type: 'string', required: false, - helpText: 'Name for the new playlist (if not using existing)' + helpText: 'Name for the new playlist (if not using existing)', }, { key: 'screen_id', @@ -405,8 +405,8 @@ const completeWorkflow = { type: 'string', required: true, dynamic: 'get_screens.id.name', - helpText: 'Select the screen to assign' - } + helpText: 'Select the screen to assign', + }, ], perform: async (z, bundle) => { // Validate playlist information @@ -418,11 +418,11 @@ const completeWorkflow = { const formData = new FormData(); formData.append('title', bundle.inputData.title); formData.append('duration', bundle.inputData.duration || 10); - formData.append('tags', ZAPIER_TAG); // Tag the asset + formData.append('tags', ZAPIER_TAG); // Tag the asset const fileResponse = await z.request({ url: bundle.inputData.file, - raw: true + raw: true, }); if (fileResponse.status >= 400) { throw new Error(`Failed to fetch file: ${fileResponse.status}`); @@ -434,9 +434,9 @@ const completeWorkflow = { url: 'https://api.screenlyapp.com/api/v4/assets/', method: 'POST', headers: { - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, - body: formData + body: formData, }); const asset = utils.handleError(assetResponse, 'Failed to upload asset'); @@ -451,12 +451,12 @@ const completeWorkflow = { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, body: { name: bundle.inputData.new_playlist_name, - tags: [ZAPIER_TAG] - } + tags: [ZAPIER_TAG], + }, }); const playlist = utils.handleError(playlistResponse, 'Failed to create playlist'); @@ -469,12 +469,12 @@ const completeWorkflow = { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, body: { asset: asset.id, - playlist: playlistId - } + playlist: playlistId, + }, }); utils.handleError(playlistItemResponse, 'Failed to add asset to playlist'); @@ -485,11 +485,11 @@ const completeWorkflow = { method: 'PATCH', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${bundle.authData.api_key}` + Authorization: `Token ${bundle.authData.api_key}`, }, body: { - playlist: playlistId - } + playlist: playlistId, + }, }); utils.handleError(screenResponse, 'Failed to assign playlist to screen'); @@ -497,16 +497,16 @@ const completeWorkflow = { return { asset: asset, playlist_id: playlistId, - screen_id: bundle.inputData.screen_id + screen_id: bundle.inputData.screen_id, }; }, sample: { asset_id: 1, playlist_id: 1, screen_id: 1, - message: 'Successfully set up complete workflow' - } - } + message: 'Successfully set up complete workflow', + }, + }, }; // Cleanup Action @@ -515,7 +515,7 @@ const cleanupZapierContent = { noun: 'Cleanup', display: { label: 'Cleanup Zapier Content', - description: 'Remove all content created by Zapier' + description: 'Remove all content created by Zapier', }, operation: { inputFields: [ @@ -524,8 +524,8 @@ const cleanupZapierContent = { label: 'Confirm Cleanup', type: 'boolean', required: true, - helpText: 'Are you sure you want to remove all content created by Zapier?' - } + helpText: 'Are you sure you want to remove all content created by Zapier?', + }, ], perform: async (z, bundle) => { if (!bundle.inputData.confirm) { @@ -537,24 +537,26 @@ const cleanupZapierContent = { url: 'https://api.screenlyapp.com/api/v4/assets/', method: 'GET', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); - const assets = utils.handleError(assetsResponse, 'Failed to fetch assets') - .filter(asset => asset.tags.includes(ZAPIER_TAG)); + const assets = utils + .handleError(assetsResponse, 'Failed to fetch assets') + .filter((asset) => asset.tags.includes(ZAPIER_TAG)); // Step 2: Get all Zapier-created playlists const playlistsResponse = await z.request({ url: 'https://api.screenlyapp.com/api/v4/playlists/', method: 'GET', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); - const playlists = utils.handleError(playlistsResponse, 'Failed to fetch playlists') - .filter(playlist => playlist.tags && playlist.tags.includes(ZAPIER_TAG)); + const playlists = utils + .handleError(playlistsResponse, 'Failed to fetch playlists') + .filter((playlist) => playlist.tags && playlist.tags.includes(ZAPIER_TAG)); // Step 3: Delete assets for (const asset of assets) { @@ -562,8 +564,8 @@ const cleanupZapierContent = { url: `https://api.screenlyapp.com/api/v4/assets/${asset.id}/`, method: 'DELETE', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); } @@ -573,22 +575,22 @@ const cleanupZapierContent = { url: `https://api.screenlyapp.com/api/v4/playlists/${playlist.id}/`, method: 'DELETE', headers: { - Authorization: `Token ${bundle.authData.api_key}` - } + Authorization: `Token ${bundle.authData.api_key}`, + }, }); } return { playlists_removed: playlists.length, - assets_removed: assets.length + assets_removed: assets.length, }; }, sample: { assets_removed: 1, playlists_removed: 1, - message: 'Successfully cleaned up Zapier content' - } - } + message: 'Successfully cleaned up Zapier content', + }, + }, }; // Export the app definition @@ -601,7 +603,7 @@ module.exports = { triggers: { [getScreens.key]: getScreens, [getPlaylists.key]: getPlaylists, - [getAssets.key]: getAssets + [getAssets.key]: getAssets, }, creates: { @@ -609,10 +611,10 @@ module.exports = { [schedulePlaylistItem.key]: schedulePlaylistItem, [assignScreenToPlaylist.key]: assignScreenToPlaylist, [completeWorkflow.key]: completeWorkflow, - [cleanupZapierContent.key]: cleanupZapierContent + [cleanupZapierContent.key]: cleanupZapierContent, }, searches: {}, - resources: {} -}; \ No newline at end of file + resources: {}, +}; diff --git a/jest.config.js b/jest.config.js index 7f84026..a6bd9a4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,27 +1,20 @@ module.exports = { - testEnvironment: "node", + testEnvironment: 'node', testTimeout: 10000, - testPathIgnorePatterns: [ - "/node_modules/", - "\\.visual\\.test\\.js$" - ], - collectCoverageFrom: [ - "*.js", - "!jest.config.js", - "!jest.visual.config.js" - ], + testPathIgnorePatterns: ['/node_modules/', '\\.visual\\.test\\.js$'], + collectCoverageFrom: ['*.js', '!jest.config.js', '!jest.visual.config.js'], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, - statements: 80 - } + statements: 80, + }, }, transform: { - "^.+\\.js$": "babel-jest" + '^.+\\.js$': 'babel-jest', }, transformIgnorePatterns: [ - "node_modules/(?!(node-fetch|fetch-blob|formdata-polyfill|data-uri-to-buffer|web-streams-polyfill)/)" - ] + 'node_modules/(?!(node-fetch|fetch-blob|formdata-polyfill|data-uri-to-buffer|web-streams-polyfill)/)', + ], }; diff --git a/jest.visual.config.js b/jest.visual.config.js index 9180e13..0fbacb5 100644 --- a/jest.visual.config.js +++ b/jest.visual.config.js @@ -2,12 +2,12 @@ module.exports = { testEnvironment: 'node', testMatch: ['**/test/*.visual.test.js'], transform: { - '^.+\\.jsx?$': 'babel-jest' + '^.+\\.jsx?$': 'babel-jest', }, setupFilesAfterEnv: ['./test/setup.visual.js'], testTimeout: 30000, moduleNameMapper: { - '^(\\.{1,2}/.*)\\.js$': '$1' + '^(\\.{1,2}/.*)\\.js$': '$1', }, - testRunner: 'jest-circus/runner' -}; \ No newline at end of file + testRunner: 'jest-circus/runner', +}; diff --git a/test/index.test.js b/test/index.test.js index a4819f0..f12b060 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -20,8 +20,8 @@ describe('Authentication', () => { test('succeeds with valid API key', async () => { const bundle = { authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; nock('https://api.screenlyapp.com') @@ -36,8 +36,8 @@ describe('Authentication', () => { test('fails with invalid API key', async () => { const bundle = { authData: { - api_key: 'invalid-api-key' - } + api_key: 'invalid-api-key', + }, }; nock('https://api.screenlyapp.com') @@ -45,8 +45,7 @@ describe('Authentication', () => { .matchHeader('Authorization', 'Token invalid-api-key') .reply(401, { detail: 'Invalid token' }); - await expect(appTester(App.authentication.test, bundle)) - .rejects.toThrow(); + await expect(appTester(App.authentication.test, bundle)).rejects.toThrow(); }); }); @@ -58,19 +57,17 @@ describe('Upload Asset', () => { test('successfully uploads an asset', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { file: 'https://example.com/image.jpg', title: 'Test Image', - duration: 10 - } + duration: 10, + }, }; // Mock the file download - nock('https://example.com') - .get('/image.jpg') - .reply(200, 'fake-file-content'); + nock('https://example.com').get('/image.jpg').reply(200, 'fake-file-content'); // Mock the asset upload nock('https://api.screenlyapp.com') @@ -79,7 +76,7 @@ describe('Upload Asset', () => { .reply(201, { id: 'asset-123', title: 'Test Image', - duration: 10 + duration: 10, }); const response = await appTester(App.creates.upload_asset.operation.perform, bundle); @@ -91,21 +88,18 @@ describe('Upload Asset', () => { test('handles upload failure', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { file: 'https://example.com/image.jpg', title: 'Test Image', - duration: 10 - } + duration: 10, + }, }; - nock('https://example.com') - .get('/image.jpg') - .reply(404); + nock('https://example.com').get('/image.jpg').reply(404); - await expect(appTester(App.creates.upload_asset.operation.perform, bundle)) - .rejects.toThrow(); + await expect(appTester(App.creates.upload_asset.operation.perform, bundle)).rejects.toThrow(); }); }); @@ -117,37 +111,37 @@ describe('Schedule Playlist Item', () => { test('successfully adds asset to playlist without conditions', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { playlist_id: 'playlist-123', asset_id: 'asset-123', - duration: 15 - } + duration: 15, + }, }; // Mock the asset duration update nock('https://api.screenlyapp.com') .patch('/api/v4/assets/asset-123/', { - duration: 15 + duration: 15, }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, { id: 'asset-123', - duration: 15 + duration: 15, }); // Mock the playlist item creation nock('https://api.screenlyapp.com') .post('/api/v4/playlist-items/', { asset: 'asset-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(201, { id: 'item-123', playlist: 'playlist-123', - asset: 'asset-123' + asset: 'asset-123', }); const response = await appTester(App.creates.schedule_playlist_item.operation.perform, bundle); @@ -159,26 +153,26 @@ describe('Schedule Playlist Item', () => { test('successfully adds asset with scheduling', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { playlist_id: 'playlist-123', asset_id: 'asset-123', duration: 20, start_date: '2023-12-01T00:00:00Z', - end_date: '2023-12-31T23:59:59Z' - } + end_date: '2023-12-31T23:59:59Z', + }, }; // Mock the asset duration update nock('https://api.screenlyapp.com') .patch('/api/v4/assets/asset-123/', { - duration: 20 + duration: 20, }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, { id: 'asset-123', - duration: 20 + duration: 20, }); // Mock the playlist item creation @@ -188,8 +182,8 @@ describe('Schedule Playlist Item', () => { playlist: 'playlist-123', conditions: { start_date: '2023-12-01T00:00:00Z', - end_date: '2023-12-31T23:59:59Z' - } + end_date: '2023-12-31T23:59:59Z', + }, }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(201, { @@ -198,8 +192,8 @@ describe('Schedule Playlist Item', () => { asset: 'asset-123', conditions: { start_date: '2023-12-01T00:00:00Z', - end_date: '2023-12-31T23:59:59Z' - } + end_date: '2023-12-31T23:59:59Z', + }, }); const response = await appTester(App.creates.schedule_playlist_item.operation.perform, bundle); @@ -214,19 +208,20 @@ describe('Schedule Playlist Item', () => { inputData: { asset_id: 'asset-123', playlist_id: 'playlist-123', - duration: -1 - } + duration: -1, + }, }; nock('https://api.screenlyapp.com') .patch('/api/v4/assets/asset-123/', { - duration: -1 + duration: -1, }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(400, { detail: 'Invalid duration' }); - await expect(appTester(App.creates.schedule_playlist_item.operation.perform, bundle)) - .rejects.toThrow(); + await expect( + appTester(App.creates.schedule_playlist_item.operation.perform, bundle) + ).rejects.toThrow(); }); test('handles playlist addition failure', async () => { @@ -234,20 +229,21 @@ describe('Schedule Playlist Item', () => { authData: { api_key: TEST_API_KEY }, inputData: { asset_id: 'asset-123', - playlist_id: 'playlist-123' - } + playlist_id: 'playlist-123', + }, }; nock('https://api.screenlyapp.com') .post('/api/v4/playlist-items/', { asset: 'asset-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }) .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(404, { detail: 'Playlist not found' }); - await expect(appTester(App.creates.schedule_playlist_item.operation.perform, bundle)) - .rejects.toThrow(); + await expect( + appTester(App.creates.schedule_playlist_item.operation.perform, bundle) + ).rejects.toThrow(); }); }); @@ -256,21 +252,22 @@ describe('Helper Functions', () => { const z = { request: jest.fn().mockResolvedValue({ status: 404, - json: { error: 'Not found' } + json: { error: 'Not found' }, }), authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; - await expect(utils.makeRequest(z, 'https://api.screenlyapp.com/api/v4/test/')) - .rejects.toThrow('Screenly API Error'); + await expect(utils.makeRequest(z, 'https://api.screenlyapp.com/api/v4/test/')).rejects.toThrow( + 'Screenly API Error' + ); }); test('handleError returns json on success', async () => { const response = { status: 200, - json: { data: 'test' } + json: { data: 'test' }, }; const result = utils.handleError(response, 'Error message'); @@ -281,25 +278,25 @@ describe('Helper Functions', () => { const z = { request: jest.fn().mockResolvedValue({ status: 200, - json: { data: 'test' } + json: { data: 'test' }, }), authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; await utils.makeRequest(z, 'https://api.screenlyapp.com/api/v4/test/', { headers: { - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }); expect(z.request).toHaveBeenCalledWith({ url: 'https://api.screenlyapp.com/api/v4/test/', headers: { 'Content-Type': 'application/json', - Authorization: `Token ${TEST_API_KEY}` - } + Authorization: `Token ${TEST_API_KEY}`, + }, }); }); }); @@ -312,8 +309,8 @@ describe('Dynamic Dropdowns', () => { test('fetches playlists for dropdown', async () => { const bundle = { authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; nock('https://api.screenlyapp.com') @@ -321,7 +318,7 @@ describe('Dynamic Dropdowns', () => { .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, [ { id: 'playlist-1', name: 'Playlist 1' }, - { id: 'playlist-2', name: 'Playlist 2' } + { id: 'playlist-2', name: 'Playlist 2' }, ]); const response = await appTester(App.triggers.get_playlists.operation.perform, bundle); @@ -333,8 +330,8 @@ describe('Dynamic Dropdowns', () => { test('fetches assets for dropdown', async () => { const bundle = { authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; nock('https://api.screenlyapp.com') @@ -342,7 +339,7 @@ describe('Dynamic Dropdowns', () => { .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, [ { id: 'asset-1', title: 'Asset 1' }, - { id: 'asset-2', title: 'Asset 2' } + { id: 'asset-2', title: 'Asset 2' }, ]); const response = await appTester(App.triggers.get_assets.operation.perform, bundle); @@ -354,8 +351,8 @@ describe('Dynamic Dropdowns', () => { test('fetches screens for dropdown', async () => { const bundle = { authData: { - api_key: TEST_API_KEY - } + api_key: TEST_API_KEY, + }, }; nock('https://api.screenlyapp.com') @@ -363,7 +360,7 @@ describe('Dynamic Dropdowns', () => { .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, [ { id: 'screen-1', name: 'Screen 1' }, - { id: 'screen-2', name: 'Screen 2' } + { id: 'screen-2', name: 'Screen 2' }, ]); const response = await appTester(App.triggers.get_screens.operation.perform, bundle); @@ -385,37 +382,33 @@ describe('Complete Workflow', () => { file: 'https://example.com/test.jpg', title: 'Test Asset', playlist_id: 'playlist-123', - screen_id: 'screen-123' - } + screen_id: 'screen-123', + }, }; - nock('https://example.com') - .get('/test.jpg') - .reply(200, Buffer.from('fake-image-data')); + nock('https://example.com').get('/test.jpg').reply(200, Buffer.from('fake-image-data')); - nock('https://api.screenlyapp.com') - .post('/api/v4/assets/') - .reply(201, { - id: 'asset-123', - title: 'Test Asset' - }); + nock('https://api.screenlyapp.com').post('/api/v4/assets/').reply(201, { + id: 'asset-123', + title: 'Test Asset', + }); nock('https://api.screenlyapp.com') .post('/api/v4/playlist-items/', { asset: 'asset-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }) .reply(201, { - id: 'item-123' + id: 'item-123', }); nock('https://api.screenlyapp.com') .patch('/api/v4/screens/screen-123/', { - playlist: 'playlist-123' + playlist: 'playlist-123', }) .reply(200, { id: 'screen-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }); const response = await appTester(App.creates.complete_workflow.operation.perform, bundle); @@ -431,48 +424,44 @@ describe('Complete Workflow', () => { file: 'https://example.com/test.jpg', title: 'Test Asset', new_playlist_name: 'New Playlist', - screen_id: 'screen-123' - } + screen_id: 'screen-123', + }, }; - nock('https://example.com') - .get('/test.jpg') - .reply(200, Buffer.from('fake-image-data')); + nock('https://example.com').get('/test.jpg').reply(200, Buffer.from('fake-image-data')); - nock('https://api.screenlyapp.com') - .post('/api/v4/assets/') - .reply(201, { - id: 'asset-123', - title: 'Test Asset' - }); + nock('https://api.screenlyapp.com').post('/api/v4/assets/').reply(201, { + id: 'asset-123', + title: 'Test Asset', + }); nock('https://api.screenlyapp.com') .post('/api/v4/playlists/', { name: 'New Playlist', - tags: ['created_by_zapier'] + tags: ['created_by_zapier'], }) .reply(201, { id: 'playlist-123', name: 'New Playlist', - tags: ['created_by_zapier'] + tags: ['created_by_zapier'], }); nock('https://api.screenlyapp.com') .post('/api/v4/playlist-items/', { asset: 'asset-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }) .reply(201, { - id: 'item-123' + id: 'item-123', }); nock('https://api.screenlyapp.com') .patch('/api/v4/screens/screen-123/', { - playlist: 'playlist-123' + playlist: 'playlist-123', }) .reply(200, { id: 'screen-123', - playlist: 'playlist-123' + playlist: 'playlist-123', }); const response = await appTester(App.creates.complete_workflow.operation.perform, bundle); @@ -487,12 +476,13 @@ describe('Complete Workflow', () => { inputData: { file: 'https://example.com/test.jpg', title: 'Test Asset', - screen_id: 'screen-123' - } + screen_id: 'screen-123', + }, }; - await expect(appTester(App.creates.complete_workflow.operation.perform, bundle)) - .rejects.toThrow('Either select an existing playlist or provide a name for a new one'); + await expect( + appTester(App.creates.complete_workflow.operation.perform, bundle) + ).rejects.toThrow('Either select an existing playlist or provide a name for a new one'); }); }); @@ -504,11 +494,11 @@ describe('Cleanup', () => { test('successfully cleans up Zapier content', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { - confirm: true - } + confirm: true, + }, }; // Mock assets list with some Zapier-created assets @@ -518,7 +508,7 @@ describe('Cleanup', () => { .reply(200, [ { id: 'asset-1', title: 'Asset 1', tags: ['created_by_zapier'] }, { id: 'asset-2', title: 'Asset 2', tags: [] }, - { id: 'asset-3', title: 'Asset 3', tags: ['created_by_zapier'] } + { id: 'asset-3', title: 'Asset 3', tags: ['created_by_zapier'] }, ]); // Mock playlists list with some Zapier-created playlists @@ -527,7 +517,7 @@ describe('Cleanup', () => { .matchHeader('Authorization', `Token ${TEST_API_KEY}`) .reply(200, [ { id: 'playlist-1', name: 'Playlist 1', tags: ['created_by_zapier'] }, - { id: 'playlist-2', name: 'Playlist 2', tags: [] } + { id: 'playlist-2', name: 'Playlist 2', tags: [] }, ]); // Mock playlist deletions @@ -556,22 +546,23 @@ describe('Cleanup', () => { const bundle = { authData: { api_key: TEST_API_KEY }, inputData: { - confirm: false - } + confirm: false, + }, }; - await expect(appTester(App.creates.cleanup_zapier_content.operation.perform, bundle)) - .rejects.toThrow('Please confirm the cleanup operation'); + await expect( + appTester(App.creates.cleanup_zapier_content.operation.perform, bundle) + ).rejects.toThrow('Please confirm the cleanup operation'); }); test('handles empty lists', async () => { const bundle = { authData: { - api_key: TEST_API_KEY + api_key: TEST_API_KEY, }, inputData: { - confirm: true - } + confirm: true, + }, }; // Mock empty assets list @@ -590,4 +581,4 @@ describe('Cleanup', () => { expect(response.playlists_removed).toBe(0); expect(response.assets_removed).toBe(0); }); -}); \ No newline at end of file +}); diff --git a/test/index.visual.test.js b/test/index.visual.test.js index 54e63c8..edb7b44 100644 --- a/test/index.visual.test.js +++ b/test/index.visual.test.js @@ -22,14 +22,14 @@ describeVisual('Zapier Visual Tests', () => { '--disable-gpu', '--window-size=1280,800', '--disable-web-security', - '--disable-features=IsolateOrigins,site-per-process' + '--disable-features=IsolateOrigins,site-per-process', ], ignoreHTTPSErrors: true, defaultViewport: { width: 1280, height: 800, - deviceScaleFactor: 1 - } + deviceScaleFactor: 1, + }, }); }); @@ -99,7 +99,7 @@ describeVisual('Zapier Visual Tests', () => { type: 'png', encoding: null, omitBackground: true, - fullPage: true + fullPage: true, }); expect(Buffer.from(screenshot)).toMatchImageSnapshot(); }); @@ -165,7 +165,7 @@ describeVisual('Zapier Visual Tests', () => { type: 'png', encoding: null, omitBackground: true, - fullPage: true + fullPage: true, }); expect(Buffer.from(screenshot)).toMatchImageSnapshot(); }); @@ -203,8 +203,8 @@ describeVisual('Zapier Visual Tests', () => { type: 'png', encoding: null, omitBackground: true, - fullPage: true + fullPage: true, }); expect(Buffer.from(screenshot)).toMatchImageSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/test/setup.visual.js b/test/setup.visual.js index 56ff5ee..30ed332 100644 --- a/test/setup.visual.js +++ b/test/setup.visual.js @@ -16,10 +16,10 @@ const toMatchImageSnapshot = configureToMatchImageSnapshot({ // Better error handling updatePassedSnapshot: false, blur: 2, - allowSizeMismatch: true + allowSizeMismatch: true, }); expect.extend({ toMatchImageSnapshot }); // Increase Jest timeout for visual tests -jest.setTimeout(30000); \ No newline at end of file +jest.setTimeout(30000); diff --git a/utils.js b/utils.js index 9f88577..b46b9f3 100644 --- a/utils.js +++ b/utils.js @@ -13,8 +13,8 @@ const makeRequest = async (z, url, options = {}) => { ...options, headers: { ...options.headers, - Authorization: `Token ${z.authData.api_key}` - } + Authorization: `Token ${z.authData.api_key}`, + }, }); return handleError(response, 'Screenly API Error'); @@ -22,5 +22,5 @@ const makeRequest = async (z, url, options = {}) => { module.exports = { handleError, - makeRequest -}; \ No newline at end of file + makeRequest, +};