mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Compare commits
18 commits
f2e8d03fb2
...
5f3a994867
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f3a994867 | ||
|
|
699b103753 | ||
|
|
0d98ac4312 | ||
|
|
ed350971ee | ||
|
|
c18b09181e | ||
|
|
7c1c42dfc1 | ||
|
|
7afc399724 | ||
|
|
3f22162cf0 | ||
|
|
2206622726 | ||
|
|
9bcd522e25 | ||
|
|
6a6c3c938f | ||
|
|
59a4d760bf | ||
|
|
fbdf630502 | ||
|
|
c74ba7d1fe | ||
|
|
6ec24ffc3d | ||
|
|
b7aa05f4ea | ||
|
|
f5c399a8cc | ||
|
|
66bbb17992 |
18 changed files with 558 additions and 128 deletions
|
|
@ -1 +1 @@
|
|||
0.29.2
|
||||
0.30.1
|
||||
|
|
|
|||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -4,10 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# [0.30.1] - 2025-07-21
|
||||
|
||||
# [0.29.2] - 2025-07-12
|
||||
## Fixed
|
||||
|
||||
⚠️ If you were using RC, please run the following commands in the console, otherwise read on. ⚠️
|
||||
- Points limit exceeded check is now cached.
|
||||
- Reverse geocoding for places is now significantly faster.
|
||||
|
||||
|
||||
# [0.30.0] - 2025-07-21
|
||||
|
||||
⚠️ If you were using 0.29.2 RC, please run the following commands in the console, otherwise read on. ⚠️
|
||||
|
||||
```ruby
|
||||
# This will delete all tracks 👇
|
||||
|
|
@ -67,11 +74,14 @@ end
|
|||
}
|
||||
}
|
||||
```
|
||||
- Links in emails will be based on the `DOMAIN` environment variable instead of `SMTP_DOMAIN`.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Swagger documentation is now valid again.
|
||||
- Invalid owntracks points are now ignored.
|
||||
- An older Owntrack's .rec format is now also supported.
|
||||
- Course and course accuracy are now rounded to 8 decimal places to fix the issue with points creation.
|
||||
|
||||
# [0.29.1] - 2025-07-02
|
||||
|
||||
|
|
|
|||
|
|
@ -36,17 +36,17 @@ class MapController < ApplicationController
|
|||
end
|
||||
|
||||
def calculate_distance
|
||||
total_distance_meters = 0
|
||||
total_distance = 0
|
||||
|
||||
@coordinates.each_cons(2) do
|
||||
distance_km = Geocoder::Calculations.distance_between(
|
||||
[_1[0], _1[1]], [_2[0], _2[1]], units: :km
|
||||
)
|
||||
|
||||
total_distance_meters += distance_km
|
||||
total_distance += distance_km
|
||||
end
|
||||
|
||||
total_distance_meters.round
|
||||
total_distance.round
|
||||
end
|
||||
|
||||
def parsed_start_at
|
||||
|
|
|
|||
|
|
@ -9,8 +9,12 @@ class OwnTracks::RecParser
|
|||
|
||||
def call
|
||||
file.split("\n").map do |line|
|
||||
# Try tab-separated first, then fall back to whitespace-separated
|
||||
parts = line.split("\t")
|
||||
|
||||
# If tab splitting didn't work (only 1 part), try whitespace splitting
|
||||
parts = line.split(/\s+/) if parts.size == 1
|
||||
|
||||
Oj.load(parts[2]) if parts.size > 2 && parts[1].strip == '*'
|
||||
end.compact
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,13 +7,18 @@ class PointsLimitExceeded
|
|||
|
||||
def call
|
||||
return false if DawarichSettings.self_hosted?
|
||||
return true if @user.tracked_points.count >= points_limit
|
||||
|
||||
false
|
||||
Rails.cache.fetch(cache_key, expires_in: 1.day) do
|
||||
@user.tracked_points.count >= points_limit
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cache_key
|
||||
"points_limit_exceeded/#{@user.id}"
|
||||
end
|
||||
|
||||
def points_limit
|
||||
DawarichSettings::BASIC_PAID_PLAN_LIMIT
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
class AddIndexOnPlacesGeodataOsmId < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :places, "(geodata->'properties'->>'osm_id')",
|
||||
using: :btree,
|
||||
name: 'index_places_on_geodata_osm_id',
|
||||
algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
6
db/schema.rb
generated
6
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_21_204404) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
enable_extension "postgis"
|
||||
|
|
@ -77,6 +77,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
|
|||
t.index ["name"], name: "index_countries_on_name"
|
||||
end
|
||||
|
||||
create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t|
|
||||
end
|
||||
|
||||
create_table "exports", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "url"
|
||||
|
|
@ -143,6 +146,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
|
|||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.geography "lonlat", limit: {srid: 4326, type: "st_point", geographic: true}
|
||||
t.index "(((geodata -> 'properties'::text) ->> 'osm_id'::text))", name: "index_places_on_geodata_osm_id"
|
||||
t.index ["lonlat"], name: "index_places_on_lonlat", using: :gist
|
||||
end
|
||||
|
||||
|
|
|
|||
504
e2e/auth.spec.ts
504
e2e/auth.spec.ts
|
|
@ -1,6 +1,8 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TestHelpers, TEST_USERS } from './fixtures/test-helpers';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Authentication', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
|
|
@ -49,8 +51,8 @@ test.describe('Authentication', () => {
|
|||
|
||||
// Should stay on login page and show error
|
||||
await expect(page).toHaveURL(/\/users\/sign_in/);
|
||||
// Devise shows error messages - look for error indication
|
||||
const errorMessage = page.locator('#error_explanation, .alert, .flash').filter({ hasText: /invalid/i });
|
||||
// Look for flash message with error styling
|
||||
const errorMessage = page.locator('.bg-red-100, .text-red-700, .alert-error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
await expect(errorMessage).toBeVisible();
|
||||
}
|
||||
|
|
@ -139,8 +141,6 @@ test.describe('Authentication', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// NOTE: Update TEST_USERS in fixtures/test-helpers.ts with correct credentials
|
||||
// that match your localhost:3000 server setup
|
||||
test.describe('Password Management', () => {
|
||||
test('should display forgot password form', async ({ page }) => {
|
||||
await page.goto('/users/sign_in');
|
||||
|
|
@ -155,115 +155,299 @@ test.describe('Authentication', () => {
|
|||
test('should handle password reset request', async ({ page }) => {
|
||||
await page.goto('/users/password/new');
|
||||
|
||||
// Fill the email but don't submit to avoid sending actual reset emails
|
||||
// Fill the email and actually submit the form
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByRole('button', { name: 'Send me reset password instructions' }).click();
|
||||
|
||||
// Verify the form elements exist and are functional
|
||||
await expect(page.getByRole('button', { name: 'Send me reset password instructions' })).toBeVisible();
|
||||
await expect(page.getByLabel('Email')).toHaveValue(TEST_USERS.DEMO.email);
|
||||
// Wait for response and check URL
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Test form validation by clearing email and checking if button is still clickable
|
||||
await page.getByLabel('Email').fill('');
|
||||
await expect(page.getByRole('button', { name: 'Send me reset password instructions' })).toBeVisible();
|
||||
// Should redirect to login page after successful submission
|
||||
await expect(page).toHaveURL(/\/users\/sign_in/);
|
||||
|
||||
// Look for success flash message with correct Devise message
|
||||
const successMessage = page.locator('.bg-blue-100, .text-blue-700').filter({ hasText: /instructions.*reset.*password.*minutes/i });
|
||||
await expect(successMessage).toBeVisible();
|
||||
});
|
||||
|
||||
test('should change password when logged in', async ({ page }) => {
|
||||
// Manual login for this test
|
||||
test.skip('should change password when logged in', async ({ page }) => {
|
||||
const newPassword = 'newpassword123';
|
||||
const helpers = new TestHelpers(page);
|
||||
|
||||
// Use helper method for robust login
|
||||
await helpers.loginAsDemo();
|
||||
|
||||
// Navigate to account settings using helper
|
||||
await helpers.goToAccountSettings();
|
||||
|
||||
// Check password change form using actual field IDs from Rails
|
||||
await expect(page.locator('input[id="user_password"]')).toBeVisible();
|
||||
await expect(page.locator('input[id="user_password_confirmation"]')).toBeVisible();
|
||||
await expect(page.locator('input[id="user_current_password"]')).toBeVisible();
|
||||
|
||||
// Clear fields first to handle browser autocomplete issues
|
||||
await page.locator('input[id="user_password"]').clear();
|
||||
await page.locator('input[id="user_password_confirmation"]').clear();
|
||||
await page.locator('input[id="user_current_password"]').clear();
|
||||
|
||||
// Wait a bit to ensure clearing is complete
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Actually change the password
|
||||
await page.locator('input[id="user_password"]').fill(newPassword);
|
||||
await page.locator('input[id="user_password_confirmation"]').fill(newPassword);
|
||||
await page.locator('input[id="user_current_password"]').fill(TEST_USERS.DEMO.password);
|
||||
|
||||
// Submit the form
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// Wait for update to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for success flash message with multiple styling options
|
||||
const successMessage = page.locator('.bg-blue-100, .text-blue-700, .bg-green-100, .text-green-700, .alert-success').filter({ hasText: /updated.*successfully/i });
|
||||
await expect(successMessage.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Navigate back to account settings to restore password
|
||||
// (Devise might have redirected us away from the form)
|
||||
await helpers.goToAccountSettings();
|
||||
|
||||
// Clear fields first
|
||||
await page.locator('input[id="user_password"]').clear();
|
||||
await page.locator('input[id="user_password_confirmation"]').clear();
|
||||
await page.locator('input[id="user_current_password"]').clear();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Restore original password
|
||||
await page.locator('input[id="user_password"]').fill(TEST_USERS.DEMO.password);
|
||||
await page.locator('input[id="user_password_confirmation"]').fill(TEST_USERS.DEMO.password);
|
||||
await page.locator('input[id="user_current_password"]').fill(newPassword);
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// Wait for restoration to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for success message to confirm restoration
|
||||
const finalSuccessMessage = page.locator('.bg-blue-100, .text-blue-700, .bg-green-100, .text-green-700, .alert-success').filter({ hasText: /updated.*successfully/i });
|
||||
await expect(finalSuccessMessage.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify we can still login with the original password by logging out and back in
|
||||
await helpers.logout();
|
||||
|
||||
// Login with original password to verify restoration worked
|
||||
await page.goto('/users/sign_in');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
|
||||
// Navigate to account settings through user dropdown
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
await userDropdown.locator('summary').click();
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
// Wait for login to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForURL(/\/map/, { timeout: 15000 });
|
||||
|
||||
await expect(page).toHaveURL(/\/users\/edit/);
|
||||
|
||||
// Check password change form is available - be more specific with selectors
|
||||
await expect(page.locator('input[id="user_password"]')).toBeVisible();
|
||||
await expect(page.getByLabel('Current password')).toBeVisible();
|
||||
|
||||
// Test filling the form but don't submit to avoid changing the password
|
||||
await page.locator('input[id="user_password"]').fill('newpassword123');
|
||||
await page.getByLabel('Current password').fill(TEST_USERS.DEMO.password);
|
||||
|
||||
// Verify the form can be filled and update button is present
|
||||
await expect(page.getByRole('button', { name: 'Update' })).toBeVisible();
|
||||
|
||||
// Clear the password fields to avoid changing credentials
|
||||
await page.locator('input[id="user_password"]').fill('');
|
||||
// Verify we're logged in with the original password
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('Account Settings', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Fresh login for each test in this describe block
|
||||
await page.goto('/users/sign_in');
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
// Use the helper method for more robust login
|
||||
const helpers = new TestHelpers(page);
|
||||
await helpers.loginAsDemo();
|
||||
});
|
||||
|
||||
test('should display account settings page', async ({ page }) => {
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
// Wait a bit more to ensure page is fully loaded
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email }).first();
|
||||
await userDropdown.locator('summary').click();
|
||||
|
||||
// Wait for dropdown to open
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/users\/edit/);
|
||||
await expect(page.getByRole('heading', { name: 'Edit your account!' })).toBeVisible();
|
||||
|
||||
// Be more flexible with the heading text
|
||||
const headingVariations = [
|
||||
page.getByRole('heading', { name: 'Edit your account!' }),
|
||||
page.getByRole('heading', { name: /edit.*account/i }),
|
||||
page.locator('h1, h2, h3').filter({ hasText: /edit.*account/i })
|
||||
];
|
||||
|
||||
let headingFound = false;
|
||||
for (const heading of headingVariations) {
|
||||
if (await heading.isVisible()) {
|
||||
await expect(heading).toBeVisible();
|
||||
headingFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!headingFound) {
|
||||
// If no heading found, at least verify we're on the right page
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
}
|
||||
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should update email address with current password', async ({ page }) => {
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
await userDropdown.locator('summary').click();
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
let emailChanged = false;
|
||||
const newEmail = 'newemail@test.com';
|
||||
|
||||
try {
|
||||
// Wait a bit more to ensure page is fully loaded
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email }).first();
|
||||
await userDropdown.locator('summary').click();
|
||||
|
||||
// Wait for dropdown to open
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
|
||||
// Test that we can fill the form, but don't actually submit to avoid changing credentials
|
||||
await page.getByLabel('Email').fill('newemail@test.com');
|
||||
await page.getByLabel('Current password').fill(TEST_USERS.DEMO.password);
|
||||
// Wait for account page to load
|
||||
await page.waitForURL(/\/users\/edit/, { timeout: 10000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify the form elements are present and fillable, but don't submit
|
||||
await expect(page.getByRole('button', { name: 'Update' })).toBeVisible();
|
||||
// Actually change the email using the correct field ID
|
||||
await page.locator('input[id="user_email"]').fill(newEmail);
|
||||
await page.locator('input[id="user_current_password"]').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// Reset the email field to avoid confusion
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
// Wait for update to complete and check for success flash message
|
||||
await page.waitForLoadState('networkidle');
|
||||
emailChanged = true;
|
||||
|
||||
// Look for success flash message with Devise styling
|
||||
const successMessage = page.locator('.bg-blue-100, .text-blue-700, .bg-green-100, .text-green-700').filter({ hasText: /updated.*successfully/i });
|
||||
await expect(successMessage.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify the new email is displayed in the navigation
|
||||
await expect(page.getByText(newEmail)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
} finally {
|
||||
// ALWAYS restore original email, even if test fails
|
||||
if (emailChanged) {
|
||||
try {
|
||||
// Navigate to account settings if not already there
|
||||
if (!page.url().includes('/users/edit')) {
|
||||
// Wait and try to find dropdown with new email
|
||||
await page.waitForTimeout(500);
|
||||
const userDropdownNew = page.locator('details').filter({ hasText: newEmail }).first();
|
||||
await userDropdownNew.locator('summary').click();
|
||||
await page.waitForTimeout(300);
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
await page.waitForURL(/\/users\/edit/, { timeout: 10000 });
|
||||
}
|
||||
|
||||
// Change email back to original
|
||||
await page.locator('input[id="user_email"]').fill(TEST_USERS.DEMO.email);
|
||||
await page.locator('input[id="user_current_password"]').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// Wait for final update to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify original email is back
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 5000 });
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to restore original email:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should view API key in settings', async ({ page }) => {
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
// Wait a bit more to ensure page is fully loaded
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email }).first();
|
||||
await userDropdown.locator('summary').click();
|
||||
|
||||
// Wait for dropdown to open
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
|
||||
// API key should be visible in the account section
|
||||
await expect(page.getByText('Use this API key')).toBeVisible();
|
||||
await expect(page.locator('code').first()).toBeVisible();
|
||||
// Wait for account page to load
|
||||
await page.waitForURL(/\/users\/edit/, { timeout: 10000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for code element containing the API key (the actual key value)
|
||||
const codeElement = page.locator('code, .code, [data-testid="api-key"]');
|
||||
await expect(codeElement.first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the API key has content
|
||||
const apiKeyValue = await codeElement.first().textContent();
|
||||
expect(apiKeyValue).toBeTruthy();
|
||||
expect(apiKeyValue?.length).toBeGreaterThan(10); // API keys should be reasonably long
|
||||
|
||||
// Verify instructional text is present (use first() to avoid strict mode issues)
|
||||
const instructionText = page.getByText('Use this API key to authenticate');
|
||||
await expect(instructionText).toBeVisible();
|
||||
});
|
||||
|
||||
test('should generate new API key', async ({ page }) => {
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
// Wait a bit more to ensure page is fully loaded
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email }).first();
|
||||
await userDropdown.locator('summary').click();
|
||||
|
||||
// Wait for dropdown to open
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
|
||||
// Wait for account page to load
|
||||
await page.waitForURL(/\/users\/edit/, { timeout: 10000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Get current API key
|
||||
const currentApiKey = await page.locator('code').first().textContent();
|
||||
|
||||
// Verify the generate new API key link exists but don't click it to avoid changing the key
|
||||
const generateKeyLink = page.getByRole('link', { name: 'Generate new API key' });
|
||||
await expect(generateKeyLink).toBeVisible();
|
||||
|
||||
// Verify the API key is displayed
|
||||
await expect(page.locator('code').first()).toBeVisible();
|
||||
const codeElement = page.locator('code, .code, [data-testid="api-key"]').first();
|
||||
await expect(codeElement).toBeVisible({ timeout: 5000 });
|
||||
const currentApiKey = await codeElement.textContent();
|
||||
expect(currentApiKey).toBeTruthy();
|
||||
|
||||
// Actually generate a new API key - be more flexible with link text
|
||||
const generateKeyLink = page.getByRole('link', { name: /generate.*new.*api.*key/i }).or(
|
||||
page.getByRole('link', { name: /regenerate.*key/i })
|
||||
);
|
||||
await expect(generateKeyLink.first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Handle the confirmation dialog if it appears
|
||||
page.on('dialog', dialog => dialog.accept());
|
||||
|
||||
await generateKeyLink.first().click();
|
||||
|
||||
// Wait for the page to reload/update
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the API key has changed
|
||||
const newApiKey = await codeElement.textContent();
|
||||
expect(newApiKey).toBeTruthy();
|
||||
expect(newApiKey).not.toBe(currentApiKey);
|
||||
|
||||
// Look for success flash message with various styling options
|
||||
const successMessage = page.locator('.bg-blue-100, .text-blue-700, .bg-green-100, .text-green-700, .alert-success');
|
||||
if (await successMessage.first().isVisible()) {
|
||||
await expect(successMessage.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should change theme', async ({ page }) => {
|
||||
// Theme toggle is in the navbar
|
||||
const themeButton = page.locator('svg').locator('..').filter({ hasText: /path/ });
|
||||
// Theme toggle is in the navbar - look for it more specifically
|
||||
const themeButton = page.locator('svg').locator('..').filter({ hasText: /path/ }).first();
|
||||
|
||||
if (await themeButton.isVisible()) {
|
||||
// Get current theme
|
||||
|
|
@ -272,12 +456,23 @@ test.describe('Authentication', () => {
|
|||
|
||||
await themeButton.click();
|
||||
|
||||
// Wait for theme change
|
||||
await page.waitForTimeout(500);
|
||||
// Wait for theme change with retry logic
|
||||
let newTheme = currentTheme;
|
||||
let attempts = 0;
|
||||
|
||||
while (newTheme === currentTheme && attempts < 10) {
|
||||
await page.waitForTimeout(200);
|
||||
newTheme = await htmlElement.getAttribute('data-theme');
|
||||
attempts++;
|
||||
}
|
||||
|
||||
// Theme should have changed
|
||||
const newTheme = await htmlElement.getAttribute('data-theme');
|
||||
expect(newTheme).not.toBe(currentTheme);
|
||||
} else {
|
||||
// If theme button is not visible, just verify the page doesn't crash
|
||||
const navbar = page.locator('.navbar');
|
||||
await expect(navbar).toBeVisible();
|
||||
console.log('Theme button not found, but navbar is functional');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -287,7 +482,7 @@ test.describe('Authentication', () => {
|
|||
await page.goto('/users/sign_in');
|
||||
|
||||
// Registration link may or may not be visible depending on SELF_HOSTED setting
|
||||
const registerLink = page.getByRole('link', { name: 'Register' }).first(); // Use first to avoid strict mode
|
||||
const registerLink = page.getByRole('link', { name: 'Register' }).first();
|
||||
const selfHosted = await page.getAttribute('html', 'data-self-hosted');
|
||||
|
||||
if (selfHosted === 'false') {
|
||||
|
|
@ -299,14 +494,22 @@ test.describe('Authentication', () => {
|
|||
|
||||
test('should display registration form when available', async ({ page }) => {
|
||||
await page.goto('/users/sign_up');
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// May redirect if self-hosted, so check current URL
|
||||
if (page.url().includes('/users/sign_up')) {
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/users/sign_up')) {
|
||||
await expect(page.getByRole('heading', { name: 'Register now!' })).toBeVisible();
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
await expect(page.locator('input[id="user_password"]')).toBeVisible(); // Be specific for main password field
|
||||
await expect(page.locator('input[id="user_password_confirmation"]')).toBeVisible(); // Use ID for confirmation field
|
||||
await expect(page.locator('input[id="user_password"]')).toBeVisible();
|
||||
await expect(page.locator('input[id="user_password_confirmation"]')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign up' })).toBeVisible();
|
||||
} else {
|
||||
// If redirected (self-hosted mode), verify we're on login page
|
||||
console.log('Registration not available (self-hosted mode), redirected to:', currentUrl);
|
||||
await expect(page).toHaveURL(/\/users\/sign_in/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -317,6 +520,9 @@ test.describe('Authentication', () => {
|
|||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
await page.goto('/users/sign_in');
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check mobile-responsive login form
|
||||
await expect(page.getByLabel('Email')).toBeVisible();
|
||||
|
|
@ -327,7 +533,26 @@ test.describe('Authentication', () => {
|
|||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
|
||||
// Wait for the form submission to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if login failed (stayed on login page)
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/users/sign_in')) {
|
||||
// Check for error messages
|
||||
const errorMessage = page.locator('.bg-red-100, .text-red-700, .alert-error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
throw new Error(`Mobile login failed for ${TEST_USERS.DEMO.email}. Credentials may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForURL(/\/map/, { timeout: 15000 });
|
||||
|
||||
// Verify we're logged in by looking for user email in navigation
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('should handle mobile navigation after login', async ({ page }) => {
|
||||
|
|
@ -335,20 +560,48 @@ test.describe('Authentication', () => {
|
|||
|
||||
// Manual login
|
||||
await page.goto('/users/sign_in');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
|
||||
// Open mobile navigation using hamburger menu
|
||||
// Wait for the form submission to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if login failed (stayed on login page)
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/users/sign_in')) {
|
||||
// Check for error messages
|
||||
const errorMessage = page.locator('.bg-red-100, .text-red-700, .alert-error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
throw new Error(`Mobile navigation login failed for ${TEST_USERS.DEMO.email}. Credentials may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForURL(/\/map/, { timeout: 15000 });
|
||||
|
||||
// Verify we're logged in first
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Open mobile navigation using hamburger menu or mobile-specific elements
|
||||
const mobileMenuButton = page.locator('label[tabindex="0"]').or(
|
||||
page.locator('button').filter({ hasText: /menu/i })
|
||||
).or(
|
||||
page.locator('.drawer-toggle')
|
||||
);
|
||||
|
||||
if (await mobileMenuButton.isVisible()) {
|
||||
await mobileMenuButton.click();
|
||||
if (await mobileMenuButton.first().isVisible()) {
|
||||
await mobileMenuButton.first().click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Should see user email in mobile menu structure
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 3000 });
|
||||
} else {
|
||||
// If mobile menu is not found, just verify the user is logged in
|
||||
console.log('Mobile menu button not found, but user is logged in');
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
|
@ -358,14 +611,36 @@ test.describe('Authentication', () => {
|
|||
|
||||
// Manual login
|
||||
await page.goto('/users/sign_in');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
|
||||
// Wait for the form submission to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if login failed (stayed on login page)
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/users/sign_in')) {
|
||||
// Check for error messages
|
||||
const errorMessage = page.locator('.bg-red-100, .text-red-700, .alert-error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
throw new Error(`Mobile logout test login failed for ${TEST_USERS.DEMO.email}. Credentials may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.waitForURL(/\/map/, { timeout: 15000 });
|
||||
|
||||
// Verify we're logged in first
|
||||
await expect(page.getByText(TEST_USERS.DEMO.email)).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// In mobile view, user dropdown should still work
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email });
|
||||
const userDropdown = page.locator('details').filter({ hasText: TEST_USERS.DEMO.email }).first();
|
||||
await userDropdown.locator('summary').click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Use evaluate to trigger the logout form submission properly
|
||||
await page.evaluate(() => {
|
||||
|
|
@ -403,21 +678,18 @@ test.describe('Authentication', () => {
|
|||
});
|
||||
|
||||
// Wait for redirect and navigate to home to verify logout
|
||||
await page.waitForURL('/', { timeout: 10000 });
|
||||
await page.waitForURL('/', { timeout: 15000 });
|
||||
|
||||
// Verify user is logged out - should see login options
|
||||
await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Sign in' })).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation Integration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Manual login for each test in this describe block
|
||||
await page.goto('/users/sign_in');
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
// Use the helper method for more robust login
|
||||
const helpers = new TestHelpers(page);
|
||||
await helpers.loginAsDemo();
|
||||
});
|
||||
|
||||
test('should show user email in navigation', async ({ page }) => {
|
||||
|
|
@ -450,34 +722,43 @@ test.describe('Authentication', () => {
|
|||
});
|
||||
|
||||
test('should show notifications dropdown', async ({ page }) => {
|
||||
// Notifications dropdown should be present - look for the notification bell icon more directly
|
||||
// Look for notifications dropdown or button with multiple approaches
|
||||
const notificationDropdown = page.locator('[data-controller="notifications"]');
|
||||
const notificationButton = page.locator('svg').filter({ hasText: /path.*stroke/ }).first();
|
||||
const bellIcon = page.locator('[data-testid="bell-icon"]');
|
||||
|
||||
// Try to find any notification-related element
|
||||
const hasNotificationDropdown = await notificationDropdown.isVisible();
|
||||
const hasNotificationButton = await notificationButton.isVisible();
|
||||
const hasBellIcon = await bellIcon.isVisible();
|
||||
|
||||
if (await notificationDropdown.isVisible()) {
|
||||
await expect(notificationDropdown).toBeVisible();
|
||||
} else {
|
||||
// Alternative: Look for notification button/bell icon
|
||||
const notificationButton = page.locator('svg').filter({ hasText: /path.*stroke.*d=/ });
|
||||
if (await notificationButton.first().isVisible()) {
|
||||
await expect(notificationButton.first()).toBeVisible();
|
||||
} else {
|
||||
// If notifications aren't available, just check that the navbar exists
|
||||
const navbar = page.locator('.navbar');
|
||||
await expect(navbar).toBeVisible();
|
||||
console.log('Notifications dropdown not found, but navbar is present');
|
||||
if (hasNotificationDropdown || hasNotificationButton || hasBellIcon) {
|
||||
// At least one notification element exists
|
||||
if (hasNotificationDropdown) {
|
||||
await expect(notificationDropdown).toBeVisible();
|
||||
} else if (hasNotificationButton) {
|
||||
await expect(notificationButton).toBeVisible();
|
||||
} else if (hasBellIcon) {
|
||||
await expect(bellIcon).toBeVisible();
|
||||
}
|
||||
console.log('Notifications feature is available');
|
||||
} else {
|
||||
// If notifications aren't available, just verify the navbar is functional
|
||||
const navbar = page.locator('.navbar');
|
||||
await expect(navbar).toBeVisible();
|
||||
console.log('Notifications feature not found, but navbar is functional');
|
||||
|
||||
// This is not necessarily an error - notifications might be disabled
|
||||
// or not implemented in this version
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Session Management', () => {
|
||||
test('should maintain session across page reloads', async ({ page }) => {
|
||||
// Manual login
|
||||
await page.goto('/users/sign_in');
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
// Use helper method for robust login
|
||||
const helpers = new TestHelpers(page);
|
||||
await helpers.loginAsDemo();
|
||||
|
||||
// Reload page
|
||||
await page.reload();
|
||||
|
|
@ -489,12 +770,9 @@ test.describe('Authentication', () => {
|
|||
});
|
||||
|
||||
test('should handle session timeout gracefully', async ({ page }) => {
|
||||
// Manual login
|
||||
await page.goto('/users/sign_in');
|
||||
await page.getByLabel('Email').fill(TEST_USERS.DEMO.email);
|
||||
await page.getByLabel('Password').fill(TEST_USERS.DEMO.password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
// Use helper method for robust login
|
||||
const helpers = new TestHelpers(page);
|
||||
await helpers.loginAsDemo();
|
||||
|
||||
// Clear all cookies to simulate session timeout
|
||||
await page.context().clearCookies();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,20 @@ export class TestHelpers {
|
|||
// Submit login
|
||||
await this.page.getByRole('button', { name: 'Log in' }).click();
|
||||
|
||||
// Wait for form submission to complete
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Check if login failed (stayed on login page with error)
|
||||
const currentUrl = this.page.url();
|
||||
if (currentUrl.includes('/users/sign_in')) {
|
||||
// Check for error messages
|
||||
const errorMessage = this.page.locator('.bg-red-100, .text-red-700, .alert-error');
|
||||
if (await errorMessage.isVisible()) {
|
||||
throw new Error(`Login failed for ${user.email}. Possible credential mismatch.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for navigation to complete - use the same approach as working tests
|
||||
await this.page.waitForURL(/\/map/, { timeout: 10000 });
|
||||
|
||||
|
|
@ -38,10 +52,28 @@ export class TestHelpers {
|
|||
}
|
||||
|
||||
/**
|
||||
* Login with demo credentials
|
||||
* Login with demo credentials with retry logic
|
||||
*/
|
||||
async loginAsDemo() {
|
||||
await this.login({ email: 'demo@dawarich.app', password: 'password' });
|
||||
// Try login with retry mechanism in case of transient failures
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
await this.login({ email: 'demo@dawarich.app', password: 'password' });
|
||||
return; // Success, exit the retry loop
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new Error(`Login failed after ${maxAttempts} attempts. Last error: ${error.message}. The demo user credentials may need to be reset. Please run: User.first.update(email: 'demo@dawarich.app', password: 'password', password_confirmation: 'password')`);
|
||||
}
|
||||
|
||||
// Wait a bit before retrying
|
||||
await this.page.waitForTimeout(1000);
|
||||
console.log(`Login attempt ${attempts} failed, retrying...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
55
e2e/global-teardown.ts
Normal file
55
e2e/global-teardown.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { chromium, FullConfig } from '@playwright/test';
|
||||
|
||||
async function globalTeardown(config: FullConfig) {
|
||||
const { baseURL } = config.projects[0].use;
|
||||
|
||||
// Launch browser for cleanup operations
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
console.log('Running global teardown - ensuring demo user credentials are restored...');
|
||||
|
||||
// Try to login with demo credentials to verify they work
|
||||
await page.goto(baseURL + '/users/sign_in');
|
||||
|
||||
await page.getByLabel('Email').fill('demo@dawarich.app');
|
||||
await page.getByLabel('Password').fill('password');
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
|
||||
// Wait for form submission
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if we successfully logged in
|
||||
const currentUrl = page.url();
|
||||
|
||||
if (currentUrl.includes('/map')) {
|
||||
console.log('Demo user credentials are working correctly');
|
||||
|
||||
// Navigate to account settings to ensure everything is properly set
|
||||
try {
|
||||
const userDropdown = page.locator('details').filter({ hasText: 'demo@dawarich.app' });
|
||||
await userDropdown.locator('summary').click();
|
||||
await page.getByRole('link', { name: 'Account' }).click();
|
||||
|
||||
// Verify account page loads
|
||||
await page.waitForURL(/\/users\/edit/, { timeout: 5000 });
|
||||
console.log('Account settings accessible - demo user is properly configured');
|
||||
} catch (e) {
|
||||
console.warn('Could not verify account settings, but login worked');
|
||||
}
|
||||
} else if (currentUrl.includes('/users/sign_in')) {
|
||||
console.warn('Demo user credentials may have been modified by tests');
|
||||
console.warn('Please run: User.first.update(email: "demo@dawarich.app", password: "password", password_confirmation: "password")');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Global teardown check failed:', error.message);
|
||||
console.warn('Demo user credentials may need to be restored manually');
|
||||
console.warn('Please run: User.first.update(email: "demo@dawarich.app", password: "password", password_confirmation: "password")');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
|
|
@ -38,6 +38,9 @@ export default defineConfig({
|
|||
|
||||
/* Global setup for checking server availability */
|
||||
globalSetup: require.resolve('./e2e/global-setup.ts'),
|
||||
|
||||
/* Global teardown for cleanup */
|
||||
globalTeardown: require.resolve('./e2e/global-teardown.ts'),
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ FactoryBot.define do
|
|||
|
||||
settings do
|
||||
{
|
||||
'route_opacity' => '0.5',
|
||||
'route_opacity' => 60,
|
||||
'meters_between_routes' => '500',
|
||||
'minutes_between_routes' => '30',
|
||||
'fog_of_war_meters' => '100',
|
||||
|
|
|
|||
10
spec/fixtures/files/owntracks/2023-02_old.rec
vendored
Normal file
10
spec/fixtures/files/owntracks/2023-02_old.rec
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
2023-02-20T18:46:22Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918783,"lat":22.0687934,"lon":24.7941786,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918782,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:25Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":13,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918785,"lat":22.0687967,"lon":24.7941813,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918785,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:25Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":13,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918790,"lat":22.0687967,"lon":24.7941813,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918785,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:35Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918795,"lat":22.0687906,"lon":24.794195,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918795,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:40Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918800,"lat":22.0687967,"lon":24.7941859,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918800,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:45Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918805,"lat":22.0687946,"lon":24.7941883,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918805,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:50Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918810,"lat":22.0687912,"lon":24.7941837,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918810,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:55Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918815,"lat":22.0687927,"lon":24.794186,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918815,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:46:55Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918815,"lat":22.0687937,"lon":24.794186,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918815,"vac":0,"vel":0,"_http":true}
|
||||
2023-02-20T18:47:00Z * {"_type":"location","BSSID":"6c:c4:9f:e0:bb:b1","SSID":"WiFi","acc":14,"alt":136,"batt":38,"bs":1,"conn":"w","created_at":1676918820,"lat":22.0687937,"lon":24.794186,"m":2,"tid":"l6","topic":"owntracks/pixel6/pixel99","tst":1676918820,"vac":0,"vel":0,"_http":true}
|
||||
|
|
@ -132,7 +132,7 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
user,
|
||||
start_at: nil,
|
||||
end_at: nil,
|
||||
mode: :bulk
|
||||
mode: :incremental
|
||||
)
|
||||
expect(generator_instance).to have_received(:call)
|
||||
expect(Notifications::Create).to have_received(:new).with(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ RSpec.describe TrackSerializer do
|
|||
let(:track) { create(:track, user: user, original_path: 'LINESTRING(0 0, 1 1, 2 2)') }
|
||||
|
||||
it 'converts geometry to WKT string format' do
|
||||
expect(serialized_track[:original_path]).to eq('LINESTRING (0 0, 1 1, 2 2)')
|
||||
expect(serialized_track[:original_path]).to match(/LINESTRING \(0(\.0)? 0(\.0)?, 1(\.0)? 1(\.0)?, 2(\.0)? 2(\.0)?\)/)
|
||||
expect(serialized_track[:original_path]).to be_a(String)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -78,5 +78,19 @@ RSpec.describe OwnTracks::Importer do
|
|||
expect(Point.first.velocity).to eq('1.4')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file is old' do
|
||||
let(:file_path) { Rails.root.join('spec/fixtures/files/owntracks/2023-02_old.rec') }
|
||||
|
||||
it 'creates points' do
|
||||
expect { parser }.to change { Point.count }.by(9)
|
||||
end
|
||||
|
||||
it 'correctly writes attributes' do
|
||||
parser
|
||||
|
||||
point = Point.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ RSpec.describe PointsLimitExceeded do
|
|||
end
|
||||
|
||||
it { is_expected.to be true }
|
||||
|
||||
it 'caches the result' do
|
||||
expect(user.tracked_points).to receive(:count).once
|
||||
2.times { described_class.new(user).call }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user points count exceeds the limit' do
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ RSpec.describe 'Map Interaction', type: :system do
|
|||
# Find and update route opacity
|
||||
within('.leaflet-settings-panel') do
|
||||
opacity_input = find('#route-opacity')
|
||||
expect(opacity_input.value).to eq('50') # Default value
|
||||
expect(opacity_input.value).to eq('60') # Default value
|
||||
|
||||
# Change opacity to 80%
|
||||
opacity_input.fill_in(with: '80')
|
||||
|
|
|
|||
Loading…
Reference in a new issue